@livenetworks/ashlar 1.6.1 → 2.0.1

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 +190 -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,15 +1,26 @@
1
- import { dispatch, computePlacement, measureHidden, registerComponent } from '../../ln-core';
1
+ import { dispatch, computePlacement, measureHidden, registerComponent, defineAttrs, attrSpec, attrStr } from '../../ln-core';
2
2
 
3
3
  (function () {
4
4
  const DOM_SELECTOR = 'data-ln-dropdown';
5
5
  const DOM_ATTRIBUTE = 'lnDropdown';
6
+ const DEFAULT_POSITION = 'bottom-end';
6
7
 
7
8
  if (window[DOM_ATTRIBUTE] !== undefined) return;
8
9
 
10
+ // ─── Attribute Contract (SSOT) ──────────────────────────
11
+ const ATTRIBUTES = {
12
+ 'data-ln-dropdown': { type: 'marker', description: 'Initializes the dropdown container' },
13
+ 'data-ln-dropdown-position': { prop: 'position', type: 'enum', values: ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end'], fallback: DEFAULT_POSITION, description: 'Preferred positioning anchor' },
14
+ 'data-ln-dropdown-placement': { type: 'enum', values: ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end'], description: 'Active calculated placement applied at runtime' },
15
+ 'data-ln-dropdown-menu': { type: 'marker', description: 'Dropdown menu element containing items' }
16
+ };
17
+ const ATTR_SPEC = attrSpec(ATTRIBUTES);
18
+
9
19
  // ─── Component ─────────────────────────────────────────────
10
20
 
11
21
  function _component(dom) {
12
22
  this.dom = dom;
23
+ defineAttrs(this, dom, ATTR_SPEC);
13
24
  this.toggleEl = dom.querySelector('[data-ln-toggle]');
14
25
  this._boundDocClick = null;
15
26
  this._docClickTimeout = null;
@@ -20,6 +31,7 @@ import { dispatch, computePlacement, measureHidden, registerComponent } from '..
20
31
  this.toggleEl.setAttribute('data-ln-dropdown-menu', '');
21
32
  this.toggleEl.setAttribute('role', 'menu');
22
33
  this.toggleEl.setAttribute('popover', 'manual');
34
+ this._initMenuAria();
23
35
  }
24
36
 
25
37
  // ARIA on trigger button
@@ -29,13 +41,6 @@ import { dispatch, computePlacement, measureHidden, registerComponent } from '..
29
41
  this.triggerBtn.setAttribute('aria-expanded', 'false');
30
42
  }
31
43
 
32
- // role="menuitem" on direct children of menu
33
- if (this.toggleEl) {
34
- for (const item of this.toggleEl.children) {
35
- item.setAttribute('role', 'menuitem');
36
- }
37
- }
38
-
39
44
  const self = this;
40
45
 
41
46
  this._onRequestOpen = function () {
@@ -51,14 +56,77 @@ import { dispatch, computePlacement, measureHidden, registerComponent } from '..
51
56
  }
52
57
  };
53
58
 
59
+ // ─── Keyboard Navigation (ARIA APG menu pattern) ───────────
60
+ this._onKeydown = function (e) {
61
+ const isOpen = self.toggleEl && self.toggleEl.getAttribute('data-ln-toggle') === 'open';
62
+
63
+ if (e.key === 'Escape') {
64
+ if (isOpen) {
65
+ e.preventDefault();
66
+ e.stopPropagation();
67
+ self.toggleEl.setAttribute('data-ln-toggle', 'close');
68
+ if (self.triggerBtn) self.triggerBtn.focus();
69
+ }
70
+ return;
71
+ }
72
+
73
+ if (e.key === 'Tab') {
74
+ if (isOpen) {
75
+ // Return focus to trigger button before closing so native Tab advance starts from trigger
76
+ if (self.triggerBtn) self.triggerBtn.focus();
77
+ self.toggleEl.setAttribute('data-ln-toggle', 'close');
78
+ }
79
+ return;
80
+ }
81
+
82
+ const items = self._getMenuItems();
83
+ if (items.length === 0) return;
84
+
85
+ // Open from trigger on ArrowDown or ArrowUp
86
+ if (!isOpen && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
87
+ e.preventDefault();
88
+ self.toggleEl.setAttribute('data-ln-toggle', 'open');
89
+ // Macrotask deferral: ensures ln-toggle's MutationObserver microtask has finished opening and showing the popover before moving focus
90
+ setTimeout(function () {
91
+ const freshItems = self._getMenuItems();
92
+ if (freshItems.length > 0) {
93
+ self._focusItem(freshItems, e.key === 'ArrowDown' ? 0 : freshItems.length - 1);
94
+ }
95
+ }, 0);
96
+ return;
97
+ }
98
+
99
+ if (!isOpen) return;
100
+
101
+ const currentIndex = items.indexOf(document.activeElement);
102
+
103
+ if (e.key === 'ArrowDown') {
104
+ e.preventDefault();
105
+ const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
106
+ self._focusItem(items, nextIndex);
107
+ } else if (e.key === 'ArrowUp') {
108
+ e.preventDefault();
109
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
110
+ self._focusItem(items, prevIndex);
111
+ } else if (e.key === 'Home') {
112
+ e.preventDefault();
113
+ self._focusItem(items, 0);
114
+ } else if (e.key === 'End') {
115
+ e.preventDefault();
116
+ self._focusItem(items, items.length - 1);
117
+ }
118
+ };
119
+
54
120
  this.dom.addEventListener('ln-dropdown:request-open', this._onRequestOpen);
55
121
  this.dom.addEventListener('ln-dropdown:request-close', this._onRequestClose);
56
122
  this.dom.addEventListener('ln-dropdown:request-toggle', this._onRequestToggle);
123
+ this.dom.addEventListener('keydown', this._onKeydown);
57
124
 
58
125
  this._onToggleOpen = function (e) {
59
126
  if (!e.detail || e.detail.target !== self.toggleEl) return;
60
127
  if (self.triggerBtn) self.triggerBtn.setAttribute('aria-expanded', 'true');
61
128
  if (typeof self.toggleEl.showPopover === 'function') self.toggleEl.showPopover();
129
+ self._initMenuAria();
62
130
  self._reposition();
63
131
  self._addOutsideClickListener();
64
132
  self._addScrollRepositionListener();
@@ -74,6 +142,7 @@ import { dispatch, computePlacement, measureHidden, registerComponent } from '..
74
142
  self._removeResizeCloseListener();
75
143
  self.toggleEl.style.top = '';
76
144
  self.toggleEl.style.left = '';
145
+ self.toggleEl.removeAttribute('data-ln-dropdown-placement');
77
146
  // :popover-open guard — a boot-opened menu (persist/static "open") was never shown via showPopover()
78
147
  if (typeof self.toggleEl.hidePopover === 'function' && self.toggleEl.matches(':popover-open')) self.toggleEl.hidePopover();
79
148
  dispatch(dom, 'ln-dropdown:close', { target: e.detail.target });
@@ -87,6 +156,35 @@ import { dispatch, computePlacement, measureHidden, registerComponent } from '..
87
156
  return this;
88
157
  }
89
158
 
159
+ // ─── Menu Items & ARIA Helpers ─────────────────────────────
160
+
161
+ _component.prototype._initMenuAria = function () {
162
+ if (!this.toggleEl) return;
163
+ const listItems = this.toggleEl.querySelectorAll('li');
164
+ for (const li of listItems) {
165
+ li.setAttribute('role', 'none');
166
+ }
167
+ const items = this._getMenuItems();
168
+ for (let i = 0; i < items.length; i++) {
169
+ items[i].setAttribute('role', 'menuitem');
170
+ items[i].setAttribute('tabindex', i === 0 ? '0' : '-1');
171
+ }
172
+ };
173
+
174
+ _component.prototype._getMenuItems = function () {
175
+ if (!this.toggleEl) return [];
176
+ return Array.from(this.toggleEl.querySelectorAll('a[href], button:not([disabled]), [role="menuitem"]:not([disabled])'));
177
+ };
178
+
179
+ _component.prototype._focusItem = function (items, index) {
180
+ for (let i = 0; i < items.length; i++) {
181
+ items[i].setAttribute('tabindex', i === index ? '0' : '-1');
182
+ }
183
+ if (items[index]) {
184
+ items[index].focus();
185
+ }
186
+ };
187
+
90
188
  // ─── Positioning ───────────────────────────────────────────
91
189
 
92
190
  _component.prototype._reposition = function () {
@@ -94,9 +192,11 @@ import { dispatch, computePlacement, measureHidden, registerComponent } from '..
94
192
  const rect = this.triggerBtn.getBoundingClientRect();
95
193
  const size = measureHidden(this.toggleEl);
96
194
  const gap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--size-xs')) * 16 || 4;
97
- const p = computePlacement(rect, size, 'bottom-end', gap);
195
+ const position = this.position || DEFAULT_POSITION;
196
+ const p = computePlacement(rect, size, position, gap);
98
197
  this.toggleEl.style.top = p.top + 'px';
99
198
  this.toggleEl.style.left = p.left + 'px';
199
+ this.toggleEl.setAttribute('data-ln-dropdown-placement', p.placement);
100
200
  };
101
201
 
102
202
  // ─── Outside click ─────────────────────────────────────────
@@ -171,6 +271,7 @@ import { dispatch, computePlacement, measureHidden, registerComponent } from '..
171
271
  this.dom.removeEventListener('ln-dropdown:request-open', this._onRequestOpen);
172
272
  this.dom.removeEventListener('ln-dropdown:request-close', this._onRequestClose);
173
273
  this.dom.removeEventListener('ln-dropdown:request-toggle', this._onRequestToggle);
274
+ this.dom.removeEventListener('keydown', this._onKeydown);
174
275
  this._removeOutsideClickListener();
175
276
  this._removeScrollRepositionListener();
176
277
  this._removeResizeCloseListener();
@@ -178,14 +279,16 @@ import { dispatch, computePlacement, measureHidden, registerComponent } from '..
178
279
  this.toggleEl.hidePopover();
179
280
  }
180
281
  if (this.toggleEl) {
282
+ this.toggleEl.removeAttribute('data-ln-dropdown-placement');
181
283
  this.toggleEl.removeEventListener('ln-toggle:open', this._onToggleOpen);
182
284
  this.toggleEl.removeEventListener('ln-toggle:close', this._onToggleClose);
183
285
  }
184
- dispatch(this.dom, 'ln-dropdown:destroyed', { target: this.dom });
185
286
  delete this.dom[DOM_ATTRIBUTE];
186
287
  };
187
288
 
188
289
  // ─── Init ──────────────────────────────────────────────────
189
290
 
190
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-dropdown');
291
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-dropdown', {
292
+ attributes: ATTRIBUTES
293
+ });
191
294
  })();
@@ -1,6 +1,9 @@
1
1
  # ln-editor
2
2
 
3
- Lightweight WYSIWYG rich text editor. Enhances a `<textarea>` into a `contentEditable` editing surface with a toolbar. Progressive enhancement: without JS, the textarea works as plain text.
3
+ > Applied to a `<div data-ln-editor>` wrapper enclosing an authored `<div role="toolbar">` and a `<textarea name="...">`.
4
+ > On initialization, it hides the textarea and injects a `<div contenteditable="true" role="textbox">` populated with the initial content.
5
+ > Toolbar clicks execute text commands on the selection; on every `input` event, it sanitizes markup and writes
6
+ > `textarea.value = editable.innerHTML` so native form submissions send current HTML.
4
7
 
5
8
  ## Quick Start
6
9
 
@@ -133,7 +136,6 @@ This authored-markup design keeps the popover structure fully customizable and a
133
136
  | `ln-editor:focus` | Notification | `{ target }` | Editing surface focused |
134
137
  | `ln-editor:blur` | Notification | `{ target }` | Editing surface blurred |
135
138
  | `ln-editor:set-content` | Request | `{ html }` | Set content programmatically — also emits `ln-editor:changed` |
136
- | `ln-editor:destroyed` | Notification | `{ target }` | Instance destroyed |
137
139
 
138
140
  ## API
139
141
 
@@ -216,9 +218,9 @@ Include only the buttons you need:
216
218
 
217
219
  Visual styling uses the two-layer architecture:
218
220
 
219
- - **Mixin:** `scss/config/mixins/_editor.scss` — `@mixin editor`
220
- - **Component:** `scss/components/_editor.scss` — applies to `[data-ln-editor]`
221
- - **Co-located:** `js/ln-editor/ln-editor.scss` — link popover (runtime-cloned from the authored `<template data-ln-template="ln-editor-link-popover">`)
221
+ - **Mixin:** `theme/config/mixins/_editor.scss` — `@mixin editor`
222
+ - **Component:** `theme/components/_editor.scss` — applies to `[data-ln-editor]`
223
+ - **Co-located:** `components/ln-editor/ln-editor.scss` — link popover (runtime-cloned from the authored `<template data-ln-template="ln-editor-link-popover">`)
222
224
 
223
225
  The editing surface uses `@include prose` — content looks identical to how it will render in a `.prose` container.
224
226
 
@@ -239,7 +241,7 @@ Override with your own selector:
239
241
 
240
242
  ## 🔧 Internals
241
243
 
242
- Source: `js/ln-editor/ln-editor.js`. No cached formatting state — every toolbar sync re-queries `document.queryCommandState` and DOM ancestry on `selectionchange`.
244
+ Source: `components/ln-editor/ln-editor.js`. No cached formatting state — every toolbar sync re-queries `document.queryCommandState` and DOM ancestry on `selectionchange`.
243
245
 
244
246
  ### Construction
245
247
 
@@ -247,7 +249,7 @@ Finds the `<textarea>`, reads its value as the initial HTML, builds a `contentEd
247
249
 
248
250
  ### Content sync
249
251
 
250
- One-way, continuous: every surface `input` event copies `innerHTML` into the textarea's `value` and dispatches `ln-editor:changed`. The textarea is never the source of truth after construction — it is a submit-time mirror.
252
+ Continuous and bidirectional: every surface `input` event copies `innerHTML` into the textarea's `value` and dispatches `ln-editor:changed`. Conversely, synthetic `input` events on the `<textarea>` (e.g. from form population via `ln-form` or coordinators) update `surface.innerHTML` and notify listeners without losing caret position during normal editing.
251
253
 
252
254
  ### Formatting flow
253
255
 
@@ -263,11 +265,11 @@ On every document `selectionchange`: bail if the selection isn't inside the surf
263
265
 
264
266
  ### Link insertion
265
267
 
266
- Requires a page-authored `<template data-ln-template="ln-editor-link-popover">` — the editor never generates this markup. Flow: save the current selection range, detect if already inside an `<a>` (pre-fill), clone the template via `cloneTemplateScoped`, insert it after the toolbar; on confirm, restore the saved range and either update the existing link's `href` or run `execCommand('createLink')` (adding `rel="noopener noreferrer"` for new links) — either path dispatches `ln-editor:changed` exactly once. A missing template makes the link action a silent no-op.
268
+ Requires a page-authored `<template data-ln-template="ln-editor-link-popover">` — the editor never generates this markup. Flow: save the current selection range, detect if already inside an `<a>` (pre-fill), clone the template via `cloneTemplateScoped`, insert it after the toolbar; on confirm, restore the saved range and either update the existing link's `href` or run `execCommand('createLink')` (adding `rel="noopener noreferrer"` for new links) — either path dispatches `ln-editor:changed` exactly once. Dismissing via outside-click closes the popover without yanking focus; explicit Cancel/Escape restores selection to the editing surface.
267
269
 
268
270
  ### Destroy
269
271
 
270
- Removes all surface/toolbar/document listeners, removes the surface node, restores textarea visibility (removes `data-ln-editor-source`), removes any open link popover, dispatches `ln-editor:destroyed`.
272
+ Removes all surface, toolbar, textarea, document, and form listeners, removes the surface node, restores textarea visibility (removes `data-ln-editor-source`), and dismisses any active link popover.
271
273
 
272
274
  ### Permanent constraints
273
275
 
@@ -0,0 +1,18 @@
1
+ @use '../../theme/config/mixins' as *;
2
+
3
+ [data-ln-debug] {
4
+ // 1. Editor Target Check: [data-ln-editor] must contain a target <textarea>
5
+ [data-ln-editor]:not(:has(textarea)) {
6
+ @include dev-dom-error("[data-ln-editor] is missing a target <textarea> element.");
7
+ }
8
+
9
+ // 2. Editor Action Check: action string cannot be empty
10
+ [data-ln-editor-action=""] {
11
+ @include dev-inline-error("data-ln-editor-action cannot be empty");
12
+ }
13
+
14
+ // 3. Editor Action Nesting Check: actions must reside inside an editor
15
+ [data-ln-editor-action]:not([data-ln-editor] *) {
16
+ @include dev-inline-error("[data-ln-editor-action] is orphaned (not inside a [data-ln-editor] container)");
17
+ }
18
+ }
@@ -0,0 +1 @@
1
+ (function(){"use strict";function B(){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 D(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 o=1;o<t.length;o++)if(t[o]&&t[o].nodeType===1){r=t[o];break}if(!D(r))return}e.apply(console,t)}}function k(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 Y(e,t,n){const r=n||{};window.lnCore._debugSink&&window.lnCore._debugSink("event",t,e,r);const o=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:r});return e.dispatchEvent(o),o}function T(e,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){T(e,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}e()}const R={};function Z(e,t){R[e]=t}function ee(e){return R[e]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=Z,window.lnCore.getDataMapper=ee);function te(){typeof window>"u"||(window.lnCore=window.lnCore||{},!window.lnCore._localeObserverBound&&(window.lnCore._localeObserverBound=!0,T(function(){new MutationObserver(function(){k(document,"ln-core:locale-change",{})}).observe(document.documentElement,{attributes:!0,attributeFilter:["lang"],subtree:!0})},"ln-core")))}const H={};function ne(e,t){if(!e||typeof t!="object")return;const n=e.toLowerCase().split("-")[0];H[n]=t}function re(e){if(!e)return null;const t=e.toLowerCase().split("-")[0];return H[t]||null}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerLocaleFallback=ne,window.lnCore.getLocaleFallback=re,window.lnCore.ensureLocaleObserver=te);const M={};function $(e,t){M[e]||(M[e]=document.querySelector('[data-ln-template="'+e+'"]'));const n=M[e];return n?n.content.cloneNode(!0):(console.warn("["+(t||"ln-core")+'] Template "'+e+'" not found'),null)}function oe(e,t,n){if(e){const r=e.querySelector('[data-ln-template="'+t+'"]');if(r)return r.content.cloneNode(!0)}return $(t,n)}function F(e,t){if(!e||!t)return e;const n=e.querySelectorAll("[data-ln-field]");for(let l=0;l<n.length;l++){const u=n[l],c=u.getAttribute("data-ln-field");t[c]!=null&&(u.textContent=t[c])}const r=e.querySelectorAll("[data-ln-attr]");for(let l=0;l<r.length;l++){const u=r[l],c=u.getAttribute("data-ln-attr").split(",");for(let h=0;h<c.length;h++){const m=c[h].trim().split(":");if(m.length!==2)continue;const g=m[0].trim(),v=m[1].trim();t[v]!=null&&u.setAttribute(g,t[v])}}const o=e.querySelectorAll("[data-ln-show]");for(let l=0;l<o.length;l++){const u=o[l],c=u.getAttribute("data-ln-show");c in t&&u.classList.toggle("hidden",!t[c])}const i=e.querySelectorAll("[data-ln-class]");for(let l=0;l<i.length;l++){const u=i[l],c=u.getAttribute("data-ln-class").split(",");for(let h=0;h<c.length;h++){const m=c[h].trim().split(":");if(m.length!==2)continue;const g=m[0].trim(),v=m[1].trim();v in t&&u.classList.toggle(g,!!t[v])}}return e}function ie(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)F(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 P(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(l,u){return t[u]!==void 0?t[u]:""}))}const r=function(i,l){return t[l]!==void 0?t[l]:""},o=Array.from(e.querySelectorAll("*"));e.nodeType===1&&o.push(e);for(let i=0;i<o.length;i++){const l=o[i],u=l.attributes;for(let c=0;c<u.length;c++){const h=u[c];h.value.indexOf("{{")!==-1&&l.setAttribute(h.name,h.value.replace(/\{\{\s*(\w+)\s*\}\}/g,r))}}return e}function le(e,t,n,r,o,i){const l={};for(let c=0;c<e.children.length;c++){const h=e.children[c],m=h.getAttribute("data-ln-render-key");m&&(l[m]=h)}const u=document.createDocumentFragment();for(let c=0;c<t.length;c++){const h=t[c],m=String(r(h));let g=l[m];if(g)o(g,h,c);else{const v=$(n,i);if(!v||(P(v,h),g=v.firstElementChild,!g))continue;g.setAttribute("data-ln-render-key",m),o(g,h,c)}u.appendChild(g)}e.textContent="",e.appendChild(u)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.fillTemplate=P,window.lnCore.fill=F,window.lnCore.lnFill=ie,window.lnCore.renderList=le);function W(e,t,n,r,o){if(!e||t===null)return!0;const i=r||"ln-component",l=e.type;if(l==="trigger"||l==="marker"||l==="string"||l==="list"||!l||t===""&&e.fallback!==void 0)return!0;const u=c=>{o?console.warn(c,o):console.warn(c)};if(l==="boolean"){const c=t.trim().toLowerCase();return c!==""&&c!=="true"&&c!=="false"&&c!=="1"&&c!=="0"?(u(`[${i}] Invalid value "${t}" for boolean attribute "${n}". Allowed: "true", "false", or presence-only.`),!1):!0}if(l==="enum"){const c=e.values||[];return c.includes(t)?!0:(u(`[${i}] Invalid value "${t}" for attribute "${n}". Allowed: ${c.join(", ")}. Fallback: "${e.fallback}".`),!1)}if(l==="integer"){if(!/^-?\d+$/.test(t))return u(`[${i}] Invalid integer "${t}" for attribute "${n}". Fallback: ${e.fallback}.`),!1;const c=parseInt(t,10);return e.min!==void 0&&c<e.min?(u(`[${i}] Value ${c} for attribute "${n}" is less than min (${e.min}).`),!1):e.max!==void 0&&c>e.max?(u(`[${i}] Value ${c} for attribute "${n}" is greater than max (${e.max}).`),!1):!0}if(l==="float")return isNaN(Number(t))?(u(`[${i}] Invalid float "${t}" for attribute "${n}". Fallback: ${e.fallback}.`),!1):!0;if(l==="json")try{return JSON.parse(t),!0}catch(c){return u(`[${i}] Invalid JSON for attribute "${n}": ${c.message}. Fallback: ${e.fallback}.`),!1}return!0}function se(e){const t={};for(const n in e){const r=e[n];r&&r.effect&&(t[n]=r.effect)}return Object.keys(t).length?t:null}function ae(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 q(e,t,n,r){if(e.nodeType!==1)return;const i=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",l=Array.from(e.querySelectorAll(i));e.matches&&e.matches(i)&&l.push(e);for(const u of l)if(!u[n]){window.lnCore._persistSink&&u.hasAttribute("data-ln-persist")&&window.lnCore._persistSink(u,t);try{u[n]=new r(u)}catch(c){console.error("["+n+"] init failed",u,c)}}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._bootHolds=window.lnCore._bootHolds||0,window.lnCore._bootQueue=window.lnCore._bootQueue||[]);function ce(){return typeof window<"u"&&window.lnCore&&window.lnCore._bootHolds||0}function ue(e){typeof window<"u"?(window.lnCore=window.lnCore||{},window.lnCore._bootHolds=window.lnCore._bootHolds||0,window.lnCore._bootQueue=window.lnCore._bootQueue||[],window.lnCore._bootHolds>0?window.lnCore._bootQueue.push(e):setTimeout(e,0)):e()}function j(){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 de(e){const t=j(),n=e.observed||[];for(let r=0;r<n.length;r++){const o=n[r];t.byAttr.has(o)||t.byAttr.set(o,[]),t.byAttr.get(o).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 o=t.byDeclaredAttr.get(r);o.some(i=>i.componentTag===e.componentTag)||o.push({spec:e.attributes[r],componentTag:e.componentTag})}if(e.onAttrChange||e.effects){t.reactive.push(e);const r=ae(e);if(r===null)t.reactiveWildcard.push(e);else for(const o of r)t.byReactive.has(o)||t.byReactive.set(o,[]),t.byReactive.get(o).push(e)}e.persist&&t.persist.push(e)}function K(e,t,n,r){for(let o=0;o<e.length;o++){const i=e[o];if(!t[i.attribute])continue;const l=i.effects&&i.effects[n];l?l(t,n,r):i.onAttrChange&&(!i.declared||i.declared.has(n))&&i.onAttrChange(t,n,r)}}function fe(e){const t=e.target,n=e.attributeName;if(e.oldValue===t.getAttribute(n))return;const r=j(),o=r.byAttr.get(n);if(B()&&r.byDeclaredAttr&&r.byDeclaredAttr.has(n)&&D(t)){const i=r.byDeclaredAttr.get(n),l=t.getAttribute(n);for(let u=0;u<i.length;u++)W(i[u].spec,l,n,i[u].componentTag,t)}if(window.lnCore._debugSink&&(n.indexOf("data-ln-")===0||o)&&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&&K(i,t,n,e.oldValue),r.reactiveWildcard.length&&K(r.reactiveWildcard,t,n,e.oldValue)}if(o)for(let i=0;i<o.length;i++){const l=o[i];if(l.handler){l.handler(t,n,e.oldValue);continue}l.onAttributeChange&&t[l.attribute]?l.onAttributeChange(t,n):(q(t,l.selector,l.attribute,l.ComponentFn),l.onInit&&l.onInit(t))}}function he(){window.lnCore=window.lnCore||{},!window.lnCore._attrObserverBound&&(window.lnCore._attrObserverBound=!0,T(function(){new MutationObserver(function(t){for(let n=0;n<t.length;n++)try{fe(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 V(){return window.lnCore=window.lnCore||{},window.lnCore._lifecycleRegistry=window.lnCore._lifecycleRegistry||[]}function we(e){const t=V();if(t.length){if(e.target)for(let n=0;n<t.length;n++){const r=t[n];if(r.onSubtreeChange){const o=r.query,i=e.target.nodeType===1?e.target.matches(o)?e.target:e.target.closest(o):e.target.parentElement?e.target.parentElement.closest(o):null;i&&r.onSubtreeChange(i,e)}}for(let n=0;n<e.addedNodes.length;n++){const r=e.addedNodes[n];if(r.nodeType===1)for(let o=0;o<t.length;o++){const i=t[o];q(r,i.selector,i.attribute,i.ComponentFn),i.onInit&&i.onInit(r)}}for(let n=0;n<e.removedNodes.length;n++){const r=e.removedNodes[n];if(r.nodeType===1)for(let o=0;o<t.length;o++){const i=t[o],l=i.query,u=Array.from(r.querySelectorAll(l));r.matches&&r.matches(l)&&u.push(r);for(let c=0;c<u.length;c++){const h=u[c];if(!document.contains(h)){const m=h[i.attribute];if(m&&typeof m.destroy=="function")try{m.destroy()}catch(g){console.error("["+i.attribute+"] destroy failed",h,g)}delete h[i.attribute]}}}}}}function be(){window.lnCore=window.lnCore||{},!window.lnCore._lifecycleObserverBound&&(window.lnCore._lifecycleObserverBound=!0,T(function(){new MutationObserver(function(t){for(let n=0;n<t.length;n++){const r=t[n];if(r.type==="childList")try{we(r)}catch(o){console.error("[ln-core] lifecycle handler failed",r.target,o)}}}).observe(document.body,{childList:!0,subtree:!0})},"ln-core"))}function ge(e,t,n,r,o={}){const i=o.extraAttributes||[],l=o.onAttributeChange||null,u=o.onSubtreeChange||null,c=o.onInit||null,h=o.onAttrChange||null,m=o.effects||null,g=o.attributes||null,v=g?se(g):m,N=g?new Set(Object.keys(g)):null,E=o.persist||null;function x(f){const p=f||document.body;if(q(p,e,t,n),g&&B())for(const b in g){const C=g[b],w=Array.from(p.querySelectorAll("["+b+"]"));p.matches&&p.matches("["+b+"]")&&w.push(p);for(let _=0;_<w.length;_++){const A=w[_];D(A)&&W(C,A.getAttribute(b),b,r,A)}}c&&c(p)}const L=[];if(e.indexOf("[")!==-1){const f=/\[([\w-]+)/g;let p;for(;(p=f.exec(e))!==null;)L.push(p[1])}else L.push(e);de({selector:e,attribute:t,componentTag:r,attributes:g,ComponentFn:n,onInit:c,observed:L.concat(i),onAttributeChange:l,onAttrChange:h,effects:v,declared:N,persist:E}),he();const a=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]";V().push({selector:e,attribute:t,ComponentFn:n,onInit:c,onSubtreeChange:u,query:a}),be(),window[t]=x;function d(){ce()>0?ue(function(){x(document.body)}):x(document.body)}return document.readyState==="loading"?document.addEventListener("DOMContentLoaded",d):d(),x}function U(e){return(e||"").replace(/^#/,"")}function I(e){const t=e===void 0?location.hash:e,n={},r=U(t);if(!r)return n;const o=r.split("&");for(let i=0;i<o.length;i++){const l=o[i];if(!l)continue;const u=l.indexOf(":"),c=u>-1?l.slice(0,u):l,h=u>-1?l.slice(u+1):"";if(c)try{n[c]=decodeURIComponent(h)}catch{n[c]=h}}return n}function pe(e){if(!e)return null;const t=I();return e in t?t[e]:null}function me(e,t){if(!e)return;const n=I();t==null?delete n[e]:n[e]=String(t);const o=Object.keys(n).map(function(i){const l=n[i];return l===""?i:i+":"+encodeURIComponent(l)}).join("&");U(location.hash)!==o&&(location.hash=o)}function _e(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 ye(e,t){return!t||t==="none"||e===null||e===void 0?null:String(e)+"."+t}function ve(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 Ae(e,t){return!e||!Array.isArray(t)||t.length===0?null:e+":"+t.map(encodeURIComponent).join(",")}function ke(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),o=r?r.split(",").map(function(i){try{return decodeURIComponent(i)}catch{return i}}).filter(Boolean):[];return{key:n,values:o}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=I,window.lnCore.hashGet=pe,window.lnCore.hashSet=me,window.lnCore.hashLinkClick=_e,window.lnCore.resolveHashNamespace=Ce,window.lnCore.hashSortEncode=ye,window.lnCore.hashSortDecode=ve,window.lnCore.hashFilterEncode=Ae,window.lnCore.hashFilterDecode=ke),(function(){const e="data-ln-editor",t="lnEditor";if(window[t]!==void 0)return;const n={"data-ln-editor":{type:"marker",description:"Mounts rich text editor instance on container"},"data-ln-editor-action":{type:"enum",values:["bold","italic","underline","strikethrough","heading-2","heading-3","heading-4","blockquote","code","paragraph","ordered-list","unordered-list","link","unlink","clear","confirm-link","cancel-link"],description:"Rich text toolbar formatting action"},"data-ln-editor-source":{type:"string",description:"Selector or ID of backing textarea synchronized with editor content"}},r={P:!0,BR:!0,STRONG:!0,B:!0,EM:!0,I:!0,U:!0,S:!0,A:!0,UL:!0,OL:!0,LI:!0,H2:!0,H3:!0,H4:!0,BLOCKQUOTE:!0,PRE:!0,CODE:!0,DIV:!0},o={bold:"bold",italic:"italic",underline:"underline",strikethrough:"strikeThrough"},i={"heading-2":"h2","heading-3":"h3","heading-4":"h4",blockquote:"blockquote",code:"pre",paragraph:"p"},l={"ordered-list":"insertOrderedList","unordered-list":"insertUnorderedList"};let u=0;function c(s){return!!(o[s]||i[s]||l[s]||s==="link")}function h(s){this.dom=s;const a=this;if(this._textarea=s.querySelector("textarea"),!this._textarea)return console.warn("[ln-editor] No <textarea> found inside",s),this;const d=this._textarea.getAttribute("placeholder")||"";this._textarea.setAttribute("data-ln-editor-source",""),this._surface=document.createElement("div"),this._surface.className="ln-editor__surface",this._surface.setAttribute("contenteditable","true"),this._surface.setAttribute("role","textbox"),this._surface.setAttribute("aria-multiline","true"),d&&this._surface.setAttribute("data-placeholder",d);const f=this._textarea.id;if(f){const w=s.querySelector('label[for="'+f+'"]');w&&(w.id||(w.id=f+"-label"),this._surface.setAttribute("aria-labelledby",w.id))}this._surface.id=f?f+"-surface":"ln-editor-surface-"+ ++u;const p=this._textarea.value.trim();p&&(this._surface.innerHTML=p);const b=s.querySelector('[role="toolbar"]');if(b&&b.nextSibling?s.insertBefore(this._surface,b.nextSibling):s.appendChild(this._surface),b){b.setAttribute("aria-controls",this._surface.id);const w=b.querySelectorAll("[data-ln-editor-action]");for(let _=0;_<w.length;_++){const A=w[_].getAttribute("data-ln-editor-action");c(A)&&w[_].setAttribute("aria-pressed","false")}}this._onInput=function(){a._syncToTextarea(),k(a.dom,"ln-editor:changed",{html:a._textarea.value,target:a.dom})},this._onMousedownToolbar=function(w){w.target.closest("[data-ln-editor-action]")&&w.preventDefault()},this._onClickToolbar=function(w){const _=w.target.closest("[data-ln-editor-action]");if(!_)return;const A=_.getAttribute("data-ln-editor-action");a._execAction(A)},this._onPaste=function(w){v(a,w)},this._onKeydown=function(w){x(a,w)},this._onSelectionChange=function(){document.contains(a._surface)&&a._updateActiveStates()},this._onFocus=function(){k(a.dom,"ln-editor:focus",{target:a.dom})},this._onBlur=function(){a._syncToTextarea(),k(a.dom,"ln-editor:blur",{target:a.dom})},this._onTextareaInput=function(){a._surface.innerHTML!==a._textarea.value&&(a._surface.innerHTML=a._textarea.value,k(a.dom,"ln-editor:changed",{html:a._textarea.value,target:a.dom}))},this._surface.addEventListener("input",this._onInput),this._surface.addEventListener("paste",this._onPaste),this._surface.addEventListener("keydown",this._onKeydown),this._surface.addEventListener("focus",this._onFocus),this._surface.addEventListener("blur",this._onBlur),this._textarea.addEventListener("input",this._onTextareaInput),b&&(b.addEventListener("mousedown",this._onMousedownToolbar),b.addEventListener("click",this._onClickToolbar)),document.addEventListener("selectionchange",this._onSelectionChange),this._onSetContent=function(w){const _=w.detail&&w.detail.html;_!==void 0&&(a._surface.innerHTML=_,a._syncToTextarea(),k(a.dom,"ln-editor:changed",{html:a._textarea.value,target:a.dom}))},s.addEventListener("ln-editor:set-content",this._onSetContent);const C=this._textarea.form;return C&&(this._onFormReset=function(){setTimeout(function(){a._surface.innerHTML=a._textarea.value,k(s,"ln-editor:changed",{html:a._textarea.value,target:s})},0)},C.addEventListener("reset",this._onFormReset)),this}h.prototype._syncToTextarea=function(){this._textarea&&(this._textarea.value=this._surface.innerHTML)},h.prototype._execAction=function(s){if(!(!s||Y(this.dom,"ln-editor:before-change",{action:s,target:this.dom}).defaultPrevented)){if(this._surface.focus(),o[s])document.execCommand(o[s],!1,null);else if(i[s]){const d=i[s],f=m(this._surface);f&&f.toLowerCase()===d?document.execCommand("formatBlock",!1,"<p>"):document.execCommand("formatBlock",!1,"<"+d+">")}else l[s]?document.execCommand(l[s],!1,null):s==="link"?L(this):s==="unlink"?document.execCommand("unlink",!1,null):s==="clear"&&(document.execCommand("removeFormat",!1,null),document.execCommand("formatBlock",!1,"<p>"));this._syncToTextarea(),this._updateActiveStates()}},h.prototype._updateActiveStates=function(){const s=this.dom.querySelector('[role="toolbar"]');if(!s)return;const a=window.getSelection();if(!a||a.rangeCount===0)return;const d=a.anchorNode;if(!d||!this._surface.contains(d))return;const f=s.querySelectorAll("[data-ln-editor-action]");for(let p=0;p<f.length;p++){const b=f[p],C=b.getAttribute("data-ln-editor-action");let w=!1;if(o[C])try{w=document.queryCommandState(o[C])}catch{}else if(i[C]){const _=m(this._surface);w=_&&_.toLowerCase()===i[C]}else if(l[C])try{w=document.queryCommandState(l[C])}catch{}else C==="link"&&(w=!!g(a.anchorNode,"A",this._surface));c(C)&&b.setAttribute("aria-pressed",String(w)),w?b.classList.add("ln-editor-active"):b.classList.remove("ln-editor-active")}},h.prototype.getHTML=function(){return this._surface?this._surface.innerHTML:""},h.prototype.setHTML=function(s){this._surface&&(this._surface.innerHTML=s,this._syncToTextarea(),k(this.dom,"ln-editor:changed",{html:this._textarea.value,target:this.dom}))},h.prototype.destroy=function(){if(!this.dom[t])return;this._surface&&(this._surface.removeEventListener("input",this._onInput),this._surface.removeEventListener("paste",this._onPaste),this._surface.removeEventListener("keydown",this._onKeydown),this._surface.removeEventListener("focus",this._onFocus),this._surface.removeEventListener("blur",this._onBlur),this._surface.remove());const s=this.dom.querySelector('[role="toolbar"]');s&&(s.removeEventListener("mousedown",this._onMousedownToolbar),s.removeEventListener("click",this._onClickToolbar)),document.removeEventListener("selectionchange",this._onSelectionChange),this.dom.removeEventListener("ln-editor:set-content",this._onSetContent);const a=this._textarea?this._textarea.form:null;if(a&&this._onFormReset&&a.removeEventListener("reset",this._onFormReset),this._textarea&&(this._onTextareaInput&&this._textarea.removeEventListener("input",this._onTextareaInput),this._textarea.removeAttribute("data-ln-editor-source")),this._closeLinkPopover)this._closeLinkPopover();else{const d=this.dom.querySelector(".ln-editor__link-popover");d&&d.remove()}delete this.dom[t]};function m(s){const a=window.getSelection();if(!a||a.rangeCount===0)return null;let d=a.anchorNode;if(!d)return null;for(;d&&d!==s;){if(d.nodeType===1){const f=d.tagName;if(f==="H2"||f==="H3"||f==="H4"||f==="BLOCKQUOTE"||f==="PRE"||f==="P")return f}d=d.parentNode}return null}function g(s,a,d){for(;s&&s!==d;){if(s.nodeType===1&&s.tagName===a)return s;s=s.parentNode}return null}function v(s,a){a.preventDefault();let d="";if(a.clipboardData&&(d=a.clipboardData.getData("text/html"),!d)){const p=a.clipboardData.getData("text/plain");p&&(d=p.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/\n\n/g,"</p><p>").replace(/\n/g,"<br>"),d="<p>"+d+"</p>")}if(!d)return;const f=N(d);f&&document.execCommand("insertHTML",!1,f)}function N(s){const a=document.createElement("div");return a.innerHTML=s,E(a),a.innerHTML}function E(s){const a=Array.from(s.childNodes);for(let d=0;d<a.length;d++){const f=a[d];if(f.nodeType!==3){if(f.nodeType!==1){s.removeChild(f);continue}if(r[f.tagName]){const p=Array.from(f.attributes);for(let b=0;b<p.length;b++){const C=p[b].name;if(f.tagName==="A"&&C==="href"){const w=f.getAttribute("href")||"";/^(https?:|mailto:|\/|#)/.test(w)||f.removeAttribute("href")}else f.removeAttribute(C)}f.tagName==="A"&&f.setAttribute("rel","noopener noreferrer"),E(f)}else{for(;f.firstChild;)s.insertBefore(f.firstChild,f);s.removeChild(f)}}}}function x(s,a){if(!(a.ctrlKey||a.metaKey))return;let d=null;switch(a.key.toLowerCase()){case"b":d="bold";break;case"i":d="italic";break;case"u":d="underline";break;case"k":d="link";break}d&&(a.preventDefault(),s._execAction(d))}function L(s){const a=window.getSelection();if(!a||a.rangeCount===0)return;const d=g(a.anchorNode,"A",s._surface),f=a.getRangeAt(0).cloneRange();s._closeLinkPopover&&s._closeLinkPopover();const p=oe(s.dom,"ln-editor-link-popover","ln-editor");if(!p)return;const b=p.firstElementChild;if(!b)return;const C=b.querySelector('input[type="url"]'),w=b.querySelector('[data-ln-editor-action="confirm-link"]'),_=b.querySelector('[data-ln-editor-action="cancel-link"]');d&&(C.value=d.getAttribute("href")||"");const A=s.dom.querySelector('[role="toolbar"]');A?A.after(b):s.dom.insertBefore(b,s._surface),C.focus();function Q(){const y=window.getSelection();y.removeAllRanges(),y.addRange(f)}function O(){document.removeEventListener("mousedown",J),s._closeLinkPopover=null,b.remove()}function z(){const y=C.value.trim();if(O(),Q(),s._surface.focus(),y)if(d)d.setAttribute("href",y),d.setAttribute("rel","noopener noreferrer"),s._syncToTextarea(),k(s.dom,"ln-editor:changed",{html:s._textarea.value,target:s.dom});else{document.execCommand("createLink",!1,y);const S=window.getSelection();if(S&&S.anchorNode){const X=g(S.anchorNode,"A",s._surface);X&&(X.setAttribute("rel","noopener noreferrer"),s._syncToTextarea())}}else d&&document.execCommand("unlink",!1,null)}function G(){O(),Q(),s._surface.focus()}function xe(){O()}function J(y){const S=s.dom.contains(y.target)&&y.target.closest('[data-ln-editor-action="link"]');!b.contains(y.target)&&!S&&xe()}s._closeLinkPopover=O,w.addEventListener("click",z),_.addEventListener("click",G),C.addEventListener("keydown",function(y){y.key==="Enter"?(y.preventDefault(),z()):y.key==="Escape"&&(y.preventDefault(),G())}),document.addEventListener("mousedown",J)}ge(e,t,h,"ln-editor",{attributes:n})})()})();
@@ -0,0 +1,58 @@
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-editor",
4
+ "generator": "scripts/sync-ln-schemas.mjs",
5
+ "attributes": {
6
+ "data-ln-debug": {
7
+ "direction": null,
8
+ "sources": [
9
+ "ln-editor-dev.scss"
10
+ ]
11
+ },
12
+ "data-ln-editor": {
13
+ "direction": null,
14
+ "sources": [
15
+ "ln-editor-dev.scss",
16
+ "src/ln-editor.js"
17
+ ],
18
+ "type": "marker",
19
+ "description": "Mounts rich text editor instance on container"
20
+ },
21
+ "data-ln-editor-action": {
22
+ "direction": "author",
23
+ "sources": [
24
+ "ln-editor-dev.scss",
25
+ "src/ln-editor.js"
26
+ ],
27
+ "type": "enum",
28
+ "values": [
29
+ "bold",
30
+ "italic",
31
+ "underline",
32
+ "strikethrough",
33
+ "heading-2",
34
+ "heading-3",
35
+ "heading-4",
36
+ "blockquote",
37
+ "code",
38
+ "paragraph",
39
+ "ordered-list",
40
+ "unordered-list",
41
+ "link",
42
+ "unlink",
43
+ "clear",
44
+ "confirm-link",
45
+ "cancel-link"
46
+ ],
47
+ "description": "Rich text toolbar formatting action"
48
+ },
49
+ "data-ln-editor-source": {
50
+ "direction": "runtime",
51
+ "sources": [
52
+ "src/ln-editor.js"
53
+ ],
54
+ "type": "string",
55
+ "description": "Selector or ID of backing textarea synchronized with editor content"
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,13 @@
1
+ // ==========================================================================
2
+ // ln-editor — Functional Link Popover Mechanics
3
+ // ==========================================================================
4
+
5
+ .ln-editor__link-popover {
6
+ display: flex;
7
+ align-items: center;
8
+
9
+ > input {
10
+ flex: 1;
11
+ min-width: 0;
12
+ }
13
+ }
@@ -6,6 +6,13 @@ import { dispatch, dispatchCancelable, registerComponent, cloneTemplateScoped }
6
6
 
7
7
  if (window[DOM_ATTRIBUTE] !== undefined) return;
8
8
 
9
+ // ─── Attribute Contract (SSOT) ──────────────────────────
10
+ const ATTRIBUTES = {
11
+ 'data-ln-editor': { type: 'marker', description: 'Mounts rich text editor instance on container' },
12
+ 'data-ln-editor-action': { type: 'enum', values: ['bold', 'italic', 'underline', 'strikethrough', 'heading-2', 'heading-3', 'heading-4', 'blockquote', 'code', 'paragraph', 'ordered-list', 'unordered-list', 'link', 'unlink', 'clear', 'confirm-link', 'cancel-link'], description: 'Rich text toolbar formatting action' },
13
+ 'data-ln-editor-source': { type: 'string', description: 'Selector or ID of backing textarea synchronized with editor content' }
14
+ };
15
+
9
16
  // ─── Allowed tags for paste sanitization ───────────────────
10
17
  const ALLOWED_TAGS = {
11
18
  P: true, BR: true, STRONG: true, B: true, EM: true, I: true,
@@ -160,12 +167,23 @@ import { dispatch, dispatchCancelable, registerComponent, cloneTemplateScoped }
160
167
  dispatch(self.dom, 'ln-editor:blur', { target: self.dom });
161
168
  };
162
169
 
170
+ this._onTextareaInput = function () {
171
+ if (self._surface.innerHTML !== self._textarea.value) {
172
+ self._surface.innerHTML = self._textarea.value;
173
+ dispatch(self.dom, 'ln-editor:changed', {
174
+ html: self._textarea.value,
175
+ target: self.dom
176
+ });
177
+ }
178
+ };
179
+
163
180
  // ─── Bind events ──────────────────────────────────────
164
181
  this._surface.addEventListener('input', this._onInput);
165
182
  this._surface.addEventListener('paste', this._onPaste);
166
183
  this._surface.addEventListener('keydown', this._onKeydown);
167
184
  this._surface.addEventListener('focus', this._onFocus);
168
185
  this._surface.addEventListener('blur', this._onBlur);
186
+ this._textarea.addEventListener('input', this._onTextareaInput);
169
187
 
170
188
  if (toolbar) {
171
189
  toolbar.addEventListener('mousedown', this._onMousedownToolbar);
@@ -344,16 +362,22 @@ import { dispatch, dispatchCancelable, registerComponent, cloneTemplateScoped }
344
362
  form.removeEventListener('reset', this._onFormReset);
345
363
  }
346
364
 
347
- // Restore textarea visibility
365
+ // Restore textarea visibility and remove input listener
348
366
  if (this._textarea) {
367
+ if (this._onTextareaInput) {
368
+ this._textarea.removeEventListener('input', this._onTextareaInput);
369
+ }
349
370
  this._textarea.removeAttribute('data-ln-editor-source');
350
371
  }
351
372
 
352
- // Remove link popover if present
353
- const popover = this.dom.querySelector('.ln-editor__link-popover');
354
- if (popover) popover.remove();
373
+ // Close/remove link popover if present
374
+ if (this._closeLinkPopover) {
375
+ this._closeLinkPopover();
376
+ } else {
377
+ const popover = this.dom.querySelector('.ln-editor__link-popover');
378
+ if (popover) popover.remove();
379
+ }
355
380
 
356
- dispatch(this.dom, 'ln-editor:destroyed', { target: this.dom });
357
381
  delete this.dom[DOM_ATTRIBUTE];
358
382
  };
359
383
 
@@ -520,9 +544,10 @@ import { dispatch, dispatchCancelable, registerComponent, cloneTemplateScoped }
520
544
  // Save the current selection range so we can restore it
521
545
  const savedRange = sel.getRangeAt(0).cloneRange();
522
546
 
523
- // Remove any existing popover
524
- const existing = instance.dom.querySelector('.ln-editor__link-popover');
525
- if (existing) existing.remove();
547
+ // Clean up any existing active popover for this instance
548
+ if (instance._closeLinkPopover) {
549
+ instance._closeLinkPopover();
550
+ }
526
551
 
527
552
  // Clone popover template
528
553
  const fragment = cloneTemplateScoped(instance.dom, 'ln-editor-link-popover', 'ln-editor');
@@ -554,9 +579,15 @@ import { dispatch, dispatchCancelable, registerComponent, cloneTemplateScoped }
554
579
  sel.addRange(savedRange);
555
580
  }
556
581
 
582
+ function _cleanupPopover() {
583
+ document.removeEventListener('mousedown', _onDocClick);
584
+ instance._closeLinkPopover = null;
585
+ popover.remove();
586
+ }
587
+
557
588
  function _apply() {
558
589
  const url = input.value.trim();
559
- popover.remove();
590
+ _cleanupPopover();
560
591
 
561
592
  _restoreSelection();
562
593
  instance._surface.focus();
@@ -594,11 +625,24 @@ import { dispatch, dispatchCancelable, registerComponent, cloneTemplateScoped }
594
625
  }
595
626
 
596
627
  function _cancel() {
597
- popover.remove();
628
+ _cleanupPopover();
598
629
  _restoreSelection();
599
630
  instance._surface.focus();
600
631
  }
601
632
 
633
+ function _dismiss() {
634
+ _cleanupPopover();
635
+ }
636
+
637
+ function _onDocClick(e) {
638
+ const inThisLinkBtn = instance.dom.contains(e.target) && e.target.closest('[data-ln-editor-action="link"]');
639
+ if (!popover.contains(e.target) && !inThisLinkBtn) {
640
+ _dismiss();
641
+ }
642
+ }
643
+
644
+ instance._closeLinkPopover = _cleanupPopover;
645
+
602
646
  confirmBtn.addEventListener('click', _apply);
603
647
  removeBtn.addEventListener('click', _cancel);
604
648
 
@@ -611,9 +655,13 @@ import { dispatch, dispatchCancelable, registerComponent, cloneTemplateScoped }
611
655
  _cancel();
612
656
  }
613
657
  });
658
+
659
+ document.addEventListener('mousedown', _onDocClick);
614
660
  }
615
661
 
616
662
  // ─── Init ─────────────────────────────────────────────────
617
663
 
618
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-editor');
664
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-editor', {
665
+ attributes: ATTRIBUTES
666
+ });
619
667
  })();
@@ -1,6 +1,8 @@
1
1
  # ln-external-links
2
2
 
3
- Auto-decorates every cross-host `<a>` and `<area>` on the page with `target="_blank"`, merged `rel="noopener noreferrer"`, and a screen-reader hint span. Runs on page load and on every DOM mutation; no opt-in attribute, no init call, no API surface for consumers to wire.
3
+ > Runs globally across `document.body` watching all `<a>` and `<area>` elements. When an element's `href` host
4
+ > differs from `window.location.host`, it sets `target="_blank"`, merges `rel="noopener noreferrer"`,
5
+ > appends a `<span class="sr-only">(opens in new tab)</span>`, and marks it with `data-ln-external-link="processed"` to prevent redundant mutations.
4
6
 
5
7
  ## Markup anatomy
6
8
 
@@ -306,22 +308,22 @@ When loaded via the main bundle, the component automatically initializes and reg
306
308
  If you only need the external links auto-decoration functionality without the rest of the `ln-ashlar` suite, you can load the compiled standalone IIFE version:
307
309
 
308
310
  ```html
309
- <script src="js/ln-external-links/ln-external-links.js" defer></script>
311
+ <script src="components/ln-external-links/ln-external-links.js" defer></script>
310
312
  ```
311
313
 
312
314
  This self-contained script executes immediately and sets up the necessary hooks and global initializer `window.lnExternalLinks` without requiring any external dependencies.
313
315
 
314
316
  ### 3. Source & Development Path
315
317
 
316
- * **Active Development Source:** [js/ln-external-links/src/ln-external-links.js](file:///c:/laragon/www/ln-ashlar/js/ln-external-links/src/ln-external-links.js) — The authoring source code containing the ES module implementation. Any new features, bug fixes, or behavioral changes must be written here.
317
- * **Compiled Standalone:** [js/ln-external-links/ln-external-links.js](file:///c:/laragon/www/ln-ashlar/js/ln-external-links/ln-external-links.js) — The compiled distribution file generated from the source code during the build process. Do not edit this file directly.
318
+ * **Active Development Source:** [components/ln-external-links/src/ln-external-links.js](file:///c:/laragon/www/ln-ashlar/components/ln-external-links/src/ln-external-links.js) — The authoring source code containing the ES module implementation. Any new features, bug fixes, or behavioral changes must be written here.
319
+ * **Compiled Standalone:** [components/ln-external-links/ln-external-links.js](file:///c:/laragon/www/ln-ashlar/components/ln-external-links/ln-external-links.js) — The compiled distribution file generated from the source code during the build process. Do not edit this file directly.
318
320
 
319
321
  ## Related
320
322
 
321
323
  - **`@mixin ln-icon`** is unused here — there's no icon decoration on
322
324
  external links by default. If you want a "↗" indicator, add it in
323
325
  project SCSS via `a[data-ln-external-link="processed"]::after`.
324
- - **`.sr-only`** (`scss/utilities/_utilities.scss`) — the
326
+ - **`.sr-only`** (`theme/utilities/_utilities.scss`) — the
325
327
  visually-hidden utility used by the appended hint span. If your
326
328
  project does not include ln-ashlar utilities, supply your own
327
329
  `.sr-only` definition or the hint will be visible.
@@ -339,7 +341,7 @@ This self-contained script executes immediately and sets up the necessary hooks
339
341
 
340
342
  ## 🔧 Internals
341
343
 
342
- Source: `js/ln-external-links/ln-external-links.js`. A **global service** (js skill §10) — no `registerComponent`, no consumer-facing `data-ln-*` opt-in attribute. It loads, runs once, and sets up a click delegate + `MutationObserver` on `document.body`.
344
+ Source: `components/ln-external-links/ln-external-links.js`. A **global service** (js skill §10) — no `registerComponent`, no consumer-facing `data-ln-*` opt-in attribute. It loads, runs once, and sets up a click delegate + `MutationObserver` on `document.body`.
343
345
 
344
346
  ### "External" test
345
347
 
@@ -347,7 +349,7 @@ The single test: truthy `link.hostname` AND `link.hostname !== window.location.h
347
349
 
348
350
  ### Script-load lifecycle
349
351
 
350
- Both the click delegate and the observer are wrapped in `guardBody` (`ln-core/helpers.js`): if `document.body` is `null` at script eval (e.g. loaded in `<head>` without `defer`), setup re-schedules itself for `DOMContentLoaded`. The initial `_processLinks()` scan follows the same `readyState === 'loading'` check. A `window.lnExternalLinks` sentinel guards against double-execution if the script loads twice.
352
+ Both the click delegate and the observer are wrapped in `guardBody` (`ln-core`): if `document.body` is `null` at script eval (e.g. loaded in `<head>` without `defer`), setup re-schedules itself for `DOMContentLoaded`. The initial `_processLinks()` scan follows the same `readyState === 'loading'` check. A `window.lnExternalLinks` sentinel guards against double-execution if the script loads twice.
351
353
 
352
354
  ### Processing pipeline
353
355
 
@@ -377,4 +379,4 @@ The sr-only hint has no i18n hook — the string is hard-coded English. Projects
377
379
 
378
380
  ### Cross-component coordination
379
381
 
380
- Imports only `dispatch` and `guardBody` from `ln-core/helpers.js`. Does not listen for any `ln-*` event and is not signaled by `ln-ajax`/data-loading components when they inject markup — decoration is purely insertion-driven via the shared observer.
382
+ Imports only `dispatch` and `guardBody` from `ln-core`. Does not listen for any `ln-*` event and is not signaled by `ln-ajax`/data-loading components when they inject markup — decoration is purely insertion-driven via the shared observer.