@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,1493 +1,1365 @@
1
- import { cloneTemplateScoped, dispatch, requestData, fill, fillTemplate, registerComponent, readValue, createWindowCache, createBatcher, getLocale, detectValueType, compareValues } from '../../ln-core';
2
-
3
- (function () {
4
- const DOM_SELECTOR = 'data-ln-table';
5
- const DOM_ATTRIBUTE = 'lnTable';
6
- const EMPTY_TEMPLATE = 'data-ln-table-empty';
7
- // Tuning constant — duplicated in ln-data-table for component independence
8
- const VIRTUAL_THRESHOLD = 200;
9
- const BUFFER_ROWS = 15;
10
- const WINDOW_DEFAULT = 1000;
11
- const WINDOW_PAGE = 200;
12
- const FETCH_DEBOUNCE = 120;
13
-
14
- if (window[DOM_ATTRIBUTE] !== undefined) return;
15
-
16
- // Singleton — same lang for all table instances on the page
17
- const _collator = typeof Intl !== 'undefined'
18
- ? new Intl.Collator(document.documentElement.lang || undefined, { sensitivity: 'base' })
19
- : null;
20
-
21
- function _formatNum(n, dom) {
22
- if (n == null || isNaN(n)) return '';
23
- try {
24
- return new Intl.NumberFormat(getLocale(dom)).format(n);
25
- } catch (e) {
26
- return String(n);
27
- }
28
- }
29
-
30
- function _findScrollContainer(el) {
31
- let p = el.parentElement;
32
- while (p && p !== document.body && p !== document.documentElement) {
33
- const cs = getComputedStyle(p);
34
- const oy = cs.overflowY;
35
- if (oy === 'auto' || oy === 'scroll') return p;
36
- p = p.parentElement;
37
- }
38
- return null;
39
- }
40
-
41
- function _saveScroll(self) {
42
- const sc = self._scrollContainer || _findScrollContainer(self.dom);
43
- return {
44
- container: sc,
45
- top: sc ? sc.scrollTop : window.scrollY
46
- };
47
- }
48
-
49
- function _restoreScroll(state) {
50
- if (state.container) {
51
- state.container.scrollTop = state.top;
52
- } else {
53
- window.scrollTo(window.scrollX, state.top);
54
- }
55
- }
56
-
57
-
58
- // ─── Component ─────────────────────────────────────────────
59
-
60
- function _component(dom) {
61
- this.dom = dom;
62
- this.table = dom.querySelector('table');
63
- this.tbody = dom.querySelector('[data-ln-table-body]') || dom.querySelector('tbody');
64
- this.thead = dom.querySelector('thead');
65
- // Scope to the column-headers row (last <tr> in thead).
66
- const colHeaderRow = this.thead ? this.thead.querySelector('tr:last-child') : null;
67
- this.ths = colHeaderRow ? Array.from(colHeaderRow.querySelectorAll('th')) : [];
68
-
69
- // Footer elements — both modes
70
- this._totalSpan = dom.querySelector('[data-ln-table-total]');
71
- this._filteredSpan = dom.querySelector('[data-ln-table-filtered]');
72
- if (this._filteredSpan) {
73
- this._filteredWrap = this._filteredSpan.parentElement !== dom
74
- ? this._filteredSpan.parentElement
75
- : null;
76
- }
77
- this._selectedSpan = dom.querySelector('[data-ln-table-selected]');
78
- if (this._selectedSpan) {
79
- this._selectedWrap = this._selectedSpan.parentElement !== dom
80
- ? this._selectedSpan.parentElement
81
- : null;
82
- }
83
-
84
- this.isDataDriven = dom.hasAttribute('data-ln-table-source');
85
- this.name = dom.getAttribute(DOM_SELECTOR) || '';
86
- this.source = dom.getAttribute('data-ln-table-source') || '';
87
-
88
- this._data = [];
89
- this._filteredData = [];
90
- this._searchTerm = '';
91
- this._sortCol = -1;
92
- this._sortDir = null;
93
- this._columnFilters = {};
94
- this.selectedIds = new Set();
95
-
96
- // Virtual scroll state
97
- this._virtual = false;
98
- this._rowHeight = 0;
99
- this._vStart = -1;
100
- this._vEnd = -1;
101
- this._rafId = null;
102
- this._scrollHandler = null;
103
- this._scrollContainer = null;
104
- this._colgroup = null;
105
-
106
- const self = this;
107
-
108
- // --- Layer 1 Command / Request Event Handlers ---
109
-
110
- this._onSetSearch = function (e) {
111
- const term = (e.detail && e.detail.query != null ? e.detail.query : (e.detail && e.detail.term != null ? e.detail.term : '')).trim();
112
- if (self.isDataDriven) {
113
- self.currentSearch = term;
114
- dispatch(dom, 'ln-table:search', {
115
- table: self.name,
116
- query: self.currentSearch
117
- });
118
- self._requestData();
119
- } else {
120
- self._searchTerm = term.toLowerCase();
121
- self._applyFilterAndSort();
122
- self._vStart = -1;
123
- self._vEnd = -1;
124
- self._render();
125
- self._updateFooter();
126
- dispatch(dom, 'ln-table:filter', {
127
- term: self._searchTerm,
128
- matched: self._filteredData.length,
129
- total: self._data.length
130
- });
131
- }
132
- };
133
- dom.addEventListener('ln-table:set-search', this._onSetSearch);
134
-
135
- this._onSetFilter = function (e) {
136
- if (!e.detail) return;
137
- const key = e.detail.key;
138
- const values = e.detail.values;
139
-
140
- if (self.isDataDriven) {
141
- if (!values || values.length === 0) {
142
- delete self.currentFilters[key];
143
- } else {
144
- self.currentFilters[key] = values;
145
- }
146
- self._requestData();
147
- } else {
148
- if (!values || values.length === 0) {
149
- delete self._columnFilters[key];
150
- } else {
151
- const lower = [];
152
- for (let i = 0; i < values.length; i++) {
153
- lower.push(values[i].toLowerCase());
154
- }
155
- self._columnFilters[key] = lower;
156
- }
157
- self._applyFilterAndSort();
158
- self._vStart = -1;
159
- self._vEnd = -1;
160
- self._render();
161
- dispatch(dom, 'ln-table:filter', {
162
- term: self._searchTerm,
163
- matched: self._filteredData.length,
164
- total: self._data.length
165
- });
166
- }
167
- };
168
- dom.addEventListener('ln-table:set-filter', this._onSetFilter);
169
-
170
- this._onRequestClearFilters = function () {
171
- if (self.isDataDriven) {
172
- self.currentFilters = {};
173
- self.currentSearch = '';
174
- dispatch(dom, 'ln-table:clear-filters', { table: self.name });
175
- self._requestData();
176
- } else {
177
- self._searchTerm = '';
178
- self._columnFilters = {};
179
- self._applyFilterAndSort();
180
- self._vStart = -1;
181
- self._vEnd = -1;
182
- self._render();
183
- self._updateFooter();
184
- dispatch(dom, 'ln-table:filter', {
185
- term: '',
186
- matched: self._filteredData.length,
187
- total: self._data.length
188
- });
189
- }
190
- };
191
- dom.addEventListener('ln-table:request-clear-filters', this._onRequestClearFilters);
192
-
193
- // --- Selection (both modes) ---
194
- this._selectable = dom.hasAttribute('data-ln-table-selectable');
195
- this._selectableActive = false;
196
- if (this._selectable) {
197
- this._enableSelection();
198
- }
199
-
200
- if (this.isDataDriven) {
201
- this.isLoaded = false;
202
- this.totalCount = 0;
203
- this.visibleCount = 0;
204
- this.currentSort = null;
205
- this.currentFilters = {};
206
- this.currentSearch = '';
207
-
208
- this._lastTotal = 0;
209
- this._lastFiltered = 0;
210
-
211
- // Windowed (sliding-window) server-side virtualization — opt-in
212
- this._windowed = false;
213
- this._cache = null;
214
- if (this.isDataDriven && dom.hasAttribute('data-ln-table-window')) this._enterWindowedMode();
215
-
216
- // --- Event listeners ---
217
- this._onSetData = function (e) {
218
- const detail = e.detail || {};
219
- if (self._windowed) {
220
- dom.classList.remove('ln-table--loading');
221
- self._cache.ingest(detail);
222
- return;
223
- }
224
- self._data = detail.data || [];
225
- self._lastTotal = detail.total != null ? detail.total : self._data.length;
226
- self._lastFiltered = detail.filtered != null ? detail.filtered : self._data.length;
227
-
228
- self.totalCount = self._lastTotal;
229
- self.visibleCount = self._lastFiltered;
230
- self.isLoaded = true;
231
-
232
- dom.classList.remove('ln-table--loading');
233
-
234
- self._vStart = -1;
235
- self._vEnd = -1;
236
-
237
- self._applyFilterAndSort();
238
- self._render();
239
- self._updateFooter();
240
-
241
- dispatch(dom, 'ln-table:rendered', {
242
- table: self.name,
243
- total: self.totalCount,
244
- visible: self.visibleCount
245
- });
246
- };
247
- dom.addEventListener('ln-table:set-data', this._onSetData);
248
-
249
- this._onSetLoading = function (e) {
250
- const loading = e.detail && e.detail.loading;
251
- dom.classList.toggle('ln-table--loading', !!loading);
252
- if (loading) {
253
- self.isLoaded = false;
254
- }
255
- };
256
- dom.addEventListener('ln-table:set-loading', this._onSetLoading);
257
-
258
- this._onPageFailed = function (e) {
259
- if (!self._windowed || !self._cache) return;
260
- self._cache.release(e.detail && e.detail.offset);
261
- };
262
- dom.addEventListener('ln-table:page-failed', this._onPageFailed);
263
-
264
- this._onRequestRevalidate = function () {
265
- if (!self._windowed || !self._cache) return;
266
- self._cache.revalidate();
267
- };
268
- dom.addEventListener('ln-table:request-revalidate', this._onRequestRevalidate);
269
-
270
- // --- Sort ---
271
- this._onSort = function (e) {
272
- e.preventDefault();
273
- self.currentSort = e.detail.direction === 'none' ? null : { field: e.detail.field, direction: e.detail.direction };
274
- self._requestData();
275
- };
276
- dom.addEventListener('ln-sort:change', this._onSort);
277
-
278
- // D4 — windowed tables cannot select rows they have never fetched
279
- if (this._windowed && this._selectable && this._selectAllCheckbox) {
280
- this._selectAllCheckbox.classList.add('hidden');
281
- }
282
-
283
- // --- Row Click & Actions ---
284
- this._onRowClick = function (e) {
285
- if (e.target.closest('[data-ln-table-row-select]')) return;
286
- if (e.target.closest('[data-ln-table-row-action]')) return;
287
- if (e.target.closest('a') || e.target.closest('button')) return;
288
- if (e.ctrlKey || e.metaKey || e.button === 1) return;
289
-
290
- const tr = e.target.closest('[data-ln-table-row]');
291
- if (!tr) return;
292
-
293
- const id = tr.getAttribute('data-ln-table-row-id');
294
- const record = tr._lnRecord || {};
295
-
296
- dispatch(dom, 'ln-table:row-click', {
297
- table: self.name,
298
- id: id,
299
- record: record
300
- });
301
- };
302
- if (this.tbody) this.tbody.addEventListener('click', this._onRowClick);
303
-
304
- this._onRowAction = function (e) {
305
- const btn = e.target.closest('[data-ln-table-row-action]');
306
- if (!btn) return;
307
-
308
- e.stopPropagation();
309
- const tr = btn.closest('[data-ln-table-row]');
310
- if (!tr) return;
311
-
312
- const action = btn.getAttribute('data-ln-table-row-action');
313
- const id = tr.getAttribute('data-ln-table-row-id');
314
- const record = tr._lnRecord || {};
315
-
316
- dispatch(dom, 'ln-table:row-action', {
317
- table: self.name,
318
- id: id,
319
- action: action,
320
- record: record
321
- });
322
- };
323
- if (this.tbody) this.tbody.addEventListener('click', this._onRowAction);
324
-
325
- // --- Keyboard navigation (tbody rows) ---
326
- this._focusedRowIndex = -1;
327
- this._onKeydown = function (e) {
328
- if (!dom.contains(document.activeElement) && document.activeElement !== document.body) return;
329
- if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) return;
330
-
331
- const rows = self.tbody ? Array.from(self.tbody.querySelectorAll('[data-ln-table-row]')) : [];
332
- if (!rows.length) return;
333
-
334
- switch (e.key) {
335
- case 'ArrowDown':
336
- e.preventDefault();
337
- self._focusedRowIndex = Math.min(self._focusedRowIndex + 1, rows.length - 1);
338
- self._focusRow(rows);
339
- break;
340
- case 'ArrowUp':
341
- e.preventDefault();
342
- self._focusedRowIndex = Math.max(self._focusedRowIndex - 1, 0);
343
- self._focusRow(rows);
344
- break;
345
- case 'Home':
346
- e.preventDefault();
347
- self._focusedRowIndex = 0;
348
- self._focusRow(rows);
349
- break;
350
- case 'End':
351
- e.preventDefault();
352
- self._focusedRowIndex = rows.length - 1;
353
- self._focusRow(rows);
354
- break;
355
- case 'Enter':
356
- if (self._focusedRowIndex >= 0 && self._focusedRowIndex < rows.length) {
357
- e.preventDefault();
358
- const tr = rows[self._focusedRowIndex];
359
- dispatch(dom, 'ln-table:row-click', {
360
- table: self.name,
361
- id: tr.getAttribute('data-ln-table-row-id'),
362
- record: tr._lnRecord || {}
363
- });
364
- }
365
- break;
366
- case ' ':
367
- if (self._selectable && self._focusedRowIndex >= 0 && self._focusedRowIndex < rows.length) {
368
- e.preventDefault();
369
- const cb = rows[self._focusedRowIndex].querySelector('[data-ln-table-row-select]');
370
- if (cb) {
371
- cb.checked = !cb.checked;
372
- cb.dispatchEvent(new Event('change', { bubbles: true }));
373
- }
374
- }
375
- break;
376
- }
377
- };
378
- document.addEventListener('keydown', this._onKeydown);
379
-
380
- // Local hydration of initial SSR rows
381
- if (this.tbody && this.tbody.rows.length > 0) {
382
- this._parseRows();
383
- }
384
-
385
- // Initial request-data
386
- if (this._windowed) {
387
- this._kickWindowInitial();
388
- } else {
389
- dispatch(dom, 'ln-table:request-data', {
390
- table: this.name,
391
- sort: this.currentSort,
392
- filters: this.currentFilters,
393
- search: this.currentSearch
394
- });
395
- }
396
-
397
- } else {
398
- // SSR Mode
399
- this._emptyTbodyObserver = null;
400
-
401
- if (this.tbody && this.tbody.rows.length > 0) {
402
- this._parseRows();
403
- } else if (this.tbody) {
404
- this._emptyTbodyObserver = new MutationObserver(function () {
405
- if (self.tbody.rows.length > 0) {
406
- self._emptyTbodyObserver.disconnect();
407
- self._emptyTbodyObserver = null;
408
- self._parseRows();
409
- }
410
- });
411
- this._emptyTbodyObserver.observe(this.tbody, { childList: true });
412
- }
413
-
414
- this._onSort = function (e) {
415
- e.preventDefault();
416
- const direction = e.detail.direction === 'none' ? null : e.detail.direction;
417
- self._sortCol = direction === null ? -1 : e.detail.column;
418
- self._sortDir = direction;
419
- self._applyFilterAndSort();
420
- self._vStart = -1;
421
- self._vEnd = -1;
422
- self._render();
423
- dispatch(dom, 'ln-table:sorted', {
424
- column: e.detail.column,
425
- direction: e.detail.direction,
426
- matched: self._filteredData.length,
427
- total: self._data.length
428
- });
429
- };
430
- dom.addEventListener('ln-sort:change', this._onSort);
431
-
432
- this._onSearchChange = function (e) {
433
- e.preventDefault();
434
- self._onSetSearch(e);
435
- };
436
- dom.addEventListener('ln-search:change', this._onSearchChange);
437
- }
438
-
439
- return this;
440
- }
441
-
442
- // ─── Parse rows into in-memory array ───────────────────────
443
-
444
- _component.prototype._parseRows = function () {
445
- const rows = this.tbody.rows;
446
- const ths = this.ths;
447
- this._data = [];
448
-
449
- if (rows.length > 0) this._rowHeight = rows[0].offsetHeight || 40;
450
- this._lockColumnWidths();
451
-
452
- for (let i = 0; i < rows.length; i++) {
453
- const tr = rows[i];
454
- const values = [];
455
- const rawTexts = [];
456
- const searchParts = [];
457
-
458
- for (let j = 0; j < tr.cells.length; j++) {
459
- const td = tr.cells[j];
460
- const text = td.textContent.trim();
461
-
462
- values[j] = readValue(td);
463
- rawTexts[j] = text.toLowerCase();
464
-
465
- if (j < tr.cells.length - 1) searchParts.push(text.toLowerCase());
466
- }
467
-
468
- let record = null;
469
- if (this.isDataDriven) {
470
- record = {};
471
- const id = tr.getAttribute('data-ln-table-row-id');
472
- if (id != null) record.id = id;
473
-
474
- for (let j = 0; j < ths.length; j++) {
475
- const field = ths[j].getAttribute('data-ln-table-col');
476
- if (field) {
477
- const cellIndex = j;
478
- if (cellIndex < tr.cells.length) {
479
- const td = tr.cells[cellIndex];
480
- record[field] = readValue(td);
481
- }
482
- }
483
- }
484
- }
485
-
486
- this._data.push({
487
- values: values,
488
- rawTexts: rawTexts,
489
- html: tr.outerHTML,
490
- searchText: searchParts.join(' '),
491
- id: this.isDataDriven && record ? record.id : undefined,
492
- ...record
493
- });
494
- }
495
-
496
- this._filteredData = this._data.slice();
497
-
498
- if (this.isDataDriven) {
499
- this._lastTotal = this._data.length;
500
- this._lastFiltered = this._data.length;
501
- this.totalCount = this._data.length;
502
- this.visibleCount = this._data.length;
503
- this._updateFooter();
504
- }
505
-
506
- this._render();
507
-
508
- dispatch(this.dom, 'ln-table:ready', {
509
- total: this._data.length
510
- });
511
- };
512
-
513
- // ─── Filter + Sort ─────────────────────────────────────────
514
-
515
- _component.prototype._applyFilterAndSort = function () {
516
- if (this.isDataDriven) {
517
- const term = (this.currentSearch || '').trim().toLowerCase();
518
- const filters = this.currentFilters || {};
519
- const hasFilters = Object.keys(filters).length > 0;
520
-
521
- // 1. Filter
522
- this._filteredData = this._data.filter(function (row) {
523
- if (term) {
524
- let match = false;
525
- for (const key in row) {
526
- if (row.hasOwnProperty(key) && typeof row[key] === 'string' && key !== 'html' && key !== 'searchText') {
527
- if (row[key].toLowerCase().indexOf(term) !== -1) {
528
- match = true;
529
- break;
530
- }
531
- }
532
- }
533
- if (!match) return false;
534
- }
535
-
536
- if (hasFilters) {
537
- for (const field in filters) {
538
- const activeVals = filters[field];
539
- if (activeVals && activeVals.length > 0) {
540
- const val = row[field];
541
- const sVal = val != null ? String(val) : '';
542
- if (activeVals.indexOf(sVal) === -1) return false;
543
- }
544
- }
545
- }
546
- return true;
547
- });
548
-
549
- this.visibleCount = this._filteredData.length;
550
-
551
- // 2. Sort
552
- if (!this.currentSort || !this.currentSort.field || !this.currentSort.direction) return;
553
-
554
- const field = this.currentSort.field;
555
- const direction = this.currentSort.direction;
556
- const multiplier = direction === 'desc' ? -1 : 1;
557
-
558
- const values = this._filteredData.map(function (row) { return row[field]; });
559
- const type = detectValueType(values);
560
-
561
- this._filteredData.sort(function (a, b) {
562
- return compareValues(a[field], b[field], type, _collator) * multiplier;
563
- });
564
-
565
- } else {
566
- const term = this._searchTerm;
567
- const colFilters = this._columnFilters;
568
- const hasColFilters = Object.keys(colFilters).length > 0;
569
- const ths = this.ths;
570
-
571
- const colIndexByKey = {};
572
- if (hasColFilters) {
573
- for (let i = 0; i < ths.length; i++) {
574
- const filterKey = ths[i].getAttribute('data-ln-table-filter-col');
575
- if (filterKey) colIndexByKey[filterKey] = i;
576
- }
577
- }
578
-
579
- if (!term && !hasColFilters) {
580
- this._filteredData = this._data.slice();
581
- } else {
582
- this._filteredData = this._data.filter(function (row) {
583
- if (term && row.searchText.indexOf(term) === -1) return false;
584
- if (hasColFilters) {
585
- for (const key in colFilters) {
586
- const idx = colIndexByKey[key];
587
- if (idx !== undefined) {
588
- if (colFilters[key].indexOf(row.rawTexts[idx]) === -1) return false;
589
- }
590
- }
591
- }
592
- return true;
593
- });
594
- }
595
-
596
- if (this._sortCol < 0 || !this._sortDir) return;
597
-
598
- const colIndex = this._sortCol;
599
- const multiplier = this._sortDir === 'desc' ? -1 : 1;
600
- const values = this._filteredData.map(function (row) { return row.values[colIndex]; });
601
- const type = detectValueType(values);
602
-
603
- this._filteredData.sort(function (a, b) {
604
- return compareValues(a.values[colIndex], b.values[colIndex], type, _collator) * multiplier;
605
- });
606
- }
607
- };
608
-
609
- // ─── Column width locking ──────────────────────────────────
610
-
611
- _component.prototype._lockColumnWidths = function () {
612
- if (!this.table || !this.thead || this._colgroup) return;
613
-
614
- const colgroup = document.createElement('colgroup');
615
- this.ths.forEach(function (th) {
616
- const col = document.createElement('col');
617
- col.style.width = th.offsetWidth + 'px';
618
- colgroup.appendChild(col);
619
- });
620
-
621
- this.table.insertBefore(colgroup, this.table.firstChild);
622
- this.table.style.tableLayout = 'fixed';
623
- this._colgroup = colgroup;
624
- };
625
-
626
- // ─── Render ────────────────────────────────────────────────
627
-
628
- _component.prototype._render = function () {
629
- if (!this.tbody) return;
630
-
631
- if (this.isDataDriven) {
632
- if (this._windowed) { this._renderWindowed(); return; }
633
- const total = this._lastTotal;
634
- const filtered = this.visibleCount;
635
-
636
- if (total === 0) {
637
- this._disableVirtualScroll();
638
- this._showEmptyState();
639
- return;
640
- }
641
-
642
- if (this._filteredData.length === 0 || filtered === 0) {
643
- this._disableVirtualScroll();
644
- this._showEmptyState();
645
- return;
646
- }
647
-
648
- if (this._filteredData.length > VIRTUAL_THRESHOLD) {
649
- this._enableVirtualScroll();
650
- this._renderVirtual();
651
- } else {
652
- this._disableVirtualScroll();
653
- this._renderAll();
654
- }
655
- } else {
656
- const count = this._filteredData.length;
657
-
658
- if (count === 0 && (this._searchTerm || Object.keys(this._columnFilters).length > 0)) {
659
- this._disableVirtualScroll();
660
- this._showEmptyState();
661
- } else if (count > VIRTUAL_THRESHOLD) {
662
- this._enableVirtualScroll();
663
- this._renderVirtual();
664
- } else {
665
- this._disableVirtualScroll();
666
- this._renderAll();
667
- }
668
- }
669
- };
670
-
671
- _component.prototype._renderAll = function () {
672
- if (this.isDataDriven) {
673
- const data = this._filteredData;
674
- const frag = document.createDocumentFragment();
675
-
676
- for (let i = 0; i < data.length; i++) {
677
- const tr = this._buildRow(data[i]);
678
- if (!tr) break;
679
- frag.appendChild(tr);
680
- }
681
-
682
- const scrollState = _saveScroll(this);
683
- this.tbody.textContent = '';
684
- this.tbody.appendChild(frag);
685
- _restoreScroll(scrollState);
686
-
687
- if (this._selectable) this._updateSelectAll();
688
- } else {
689
- const html = [];
690
- const data = this._filteredData;
691
- for (let i = 0; i < data.length; i++) html.push(data[i].html);
692
-
693
- const scrollState = _saveScroll(this);
694
- this.tbody.innerHTML = html.join('');
695
- _restoreScroll(scrollState);
696
-
697
- if (this._selectable) this._restoreSelection();
698
- }
699
- };
700
-
701
- // ─── Virtual scroll ────────────────────────────────────────
702
-
703
- _component.prototype._enableVirtualScroll = function () {
704
- if (this._virtual) return;
705
- this._virtual = true;
706
- this._vStart = -1;
707
- this._vEnd = -1;
708
- const self = this;
709
-
710
- if (!this._rowHeight) {
711
- if (this._windowed) {
712
- let tempRow = null;
713
- const sample = this._cache.peek();
714
- if (sample) {
715
- tempRow = this._buildRow(sample);
716
- } else {
717
- tempRow = this._buildPlaceholderRow();
718
- }
719
- if (tempRow) {
720
- this.tbody.textContent = '';
721
- this.tbody.appendChild(tempRow);
722
- this._rowHeight = tempRow.offsetHeight || 40;
723
- this.tbody.textContent = '';
724
- }
725
- } else if (this.isDataDriven) {
726
- if (this._data.length > 0) {
727
- const tempRow = this._buildRow(this._data[0]);
728
- if (tempRow) {
729
- this.tbody.textContent = '';
730
- this.tbody.appendChild(tempRow);
731
- this._rowHeight = tempRow.offsetHeight || 40;
732
- this.tbody.textContent = '';
733
- }
734
- }
735
- } else {
736
- const rows = this.tbody ? this.tbody.rows : [];
737
- if (rows.length > 0) {
738
- this._rowHeight = rows[0].offsetHeight || 40;
739
- }
740
- }
741
- }
742
-
743
- if (this.isDataDriven) {
744
- this._scrollContainer = _findScrollContainer(this.dom);
745
- } else {
746
- this._scrollContainer = null;
747
- }
748
- const scrollTarget = this._scrollContainer || window;
749
-
750
- this._scrollHandler = function () {
751
- if (self._rafId) return;
752
- self._rafId = requestAnimationFrame(function () {
753
- self._rafId = null;
754
- self._windowed ? self._renderWindowed() : self._renderVirtual();
755
- });
756
- };
757
-
758
- scrollTarget.addEventListener('scroll', this._scrollHandler, { passive: true });
759
- window.addEventListener('resize', this._scrollHandler, { passive: true });
760
- };
761
-
762
- _component.prototype._disableVirtualScroll = function () {
763
- if (!this._virtual) return;
764
- this._virtual = false;
765
-
766
- if (this._scrollHandler) {
767
- const scrollTarget = this._scrollContainer || window;
768
- scrollTarget.removeEventListener('scroll', this._scrollHandler);
769
- window.removeEventListener('resize', this._scrollHandler);
770
- this._scrollHandler = null;
771
- }
772
- this._scrollContainer = null;
773
- if (this._rafId) {
774
- cancelAnimationFrame(this._rafId);
775
- this._rafId = null;
776
- }
777
- this._vStart = -1;
778
- this._vEnd = -1;
779
- };
780
-
781
- _component.prototype._renderVirtual = function () {
782
- const data = this._filteredData;
783
- const total = data.length;
784
- const rowH = this._rowHeight;
785
- if (!rowH || !total) return;
786
-
787
- const theadH = this.thead ? this.thead.offsetHeight : 0;
788
- const sc = this._scrollContainer;
789
- let scrollIntoData;
790
- let viewportH;
791
-
792
- if (sc) {
793
- const tableRect = this.table.getBoundingClientRect();
794
- const scRect = sc.getBoundingClientRect();
795
- const dataStartInContainer = (tableRect.top - scRect.top) + sc.scrollTop + theadH;
796
- scrollIntoData = sc.scrollTop - dataStartInContainer;
797
- viewportH = sc.clientHeight;
798
- } else {
799
- const tableRect = this.table.getBoundingClientRect();
800
- const tableTopInPage = tableRect.top + window.scrollY;
801
- const dataStartInPage = tableTopInPage + theadH;
802
- scrollIntoData = window.scrollY - dataStartInPage;
803
- viewportH = window.innerHeight;
804
- }
805
-
806
- let startRow = Math.max(0, Math.floor(scrollIntoData / rowH) - BUFFER_ROWS);
807
- startRow = Math.min(startRow, total);
808
- const endRow = Math.min(startRow + Math.ceil(viewportH / rowH) + (BUFFER_ROWS * 2), total);
809
-
810
- if (startRow === this._vStart && endRow === this._vEnd) return;
811
- this._vStart = startRow;
812
- this._vEnd = endRow;
813
-
814
- const colSpan = this.ths.length || 1;
815
- const topH = startRow * rowH;
816
- const bottomH = (total - endRow) * rowH;
817
-
818
- if (this.isDataDriven) {
819
- const frag = document.createDocumentFragment();
820
-
821
- if (topH > 0) {
822
- const topSpacer = document.createElement('tr');
823
- topSpacer.className = 'ln-table__spacer';
824
- topSpacer.setAttribute('aria-hidden', 'true');
825
- const topTd = document.createElement('td');
826
- topTd.setAttribute('colspan', colSpan);
827
- topTd.style.height = topH + 'px';
828
- topSpacer.appendChild(topTd);
829
- frag.appendChild(topSpacer);
830
- }
831
-
832
- for (let i = startRow; i < endRow; i++) {
833
- const tr = this._buildRow(data[i]);
834
- if (tr) frag.appendChild(tr);
835
- }
836
-
837
- if (bottomH > 0) {
838
- const bottomSpacer = document.createElement('tr');
839
- bottomSpacer.className = 'ln-table__spacer';
840
- bottomSpacer.setAttribute('aria-hidden', 'true');
841
- const bottomTd = document.createElement('td');
842
- bottomTd.setAttribute('colspan', colSpan);
843
- bottomTd.style.height = bottomH + 'px';
844
- bottomSpacer.appendChild(bottomTd);
845
- frag.appendChild(bottomSpacer);
846
- }
847
-
848
- const scrollState = _saveScroll(this);
849
- this.tbody.textContent = '';
850
- this.tbody.appendChild(frag);
851
- _restoreScroll(scrollState);
852
-
853
- if (this._selectable) this._updateSelectAll();
854
- } else {
855
- let html = '';
856
-
857
- if (topH > 0) {
858
- html += '<tr class="ln-table__spacer" aria-hidden="true"><td colspan="' +
859
- colSpan + '" style="height:' + topH + 'px;padding:0;border:none"></td></tr>';
860
- }
861
- for (let i = startRow; i < endRow; i++) html += data[i].html;
862
- if (bottomH > 0) {
863
- html += '<tr class="ln-table__spacer" aria-hidden="true"><td colspan="' +
864
- colSpan + '" style="height:' + bottomH + 'px;padding:0;border:none"></td></tr>';
865
- }
866
-
867
- const scrollState = _saveScroll(this);
868
- this.tbody.innerHTML = html;
869
- _restoreScroll(scrollState);
870
-
871
- if (this._selectable) this._restoreSelection();
872
- }
873
- };
874
-
875
- // ─── Windowed virtualization (opt-in sliding window) ───────
876
-
877
- _component.prototype._buildPlaceholderRow = function () {
878
- const tr = document.createElement('tr');
879
- tr.className = 'ln-table__placeholder';
880
- tr.setAttribute('aria-hidden', 'true');
881
- const td = document.createElement('td');
882
- td.setAttribute('colspan', this.ths.length || 1);
883
- td.style.height = this._rowHeight + 'px';
884
- tr.appendChild(td);
885
- return tr;
886
- };
887
-
888
- _component.prototype._renderWindowed = function () {
889
- if (this.isLoaded && this._cache.logicalTotal === 0) {
890
- this._disableVirtualScroll();
891
- this._showEmptyState();
892
- return;
893
- }
894
-
895
- if (!this._virtual) this._enableVirtualScroll();
896
-
897
- const rowH = this._rowHeight;
898
- if (!rowH) return;
899
-
900
- const total = this._cache.logicalTotal;
901
- const theadH = this.thead ? this.thead.offsetHeight : 0;
902
- const sc = this._scrollContainer;
903
- let scrollIntoData;
904
- let viewportH;
905
-
906
- if (sc) {
907
- const tableRect = this.table.getBoundingClientRect();
908
- const scRect = sc.getBoundingClientRect();
909
- const dataStartInContainer = (tableRect.top - scRect.top) + sc.scrollTop + theadH;
910
- scrollIntoData = sc.scrollTop - dataStartInContainer;
911
- viewportH = sc.clientHeight;
912
- } else {
913
- const tableRect = this.table.getBoundingClientRect();
914
- const tableTopInPage = tableRect.top + window.scrollY;
915
- const dataStartInPage = tableTopInPage + theadH;
916
- scrollIntoData = window.scrollY - dataStartInPage;
917
- viewportH = window.innerHeight;
918
- }
919
-
920
- let startRow = Math.max(0, Math.floor(scrollIntoData / rowH) - BUFFER_ROWS);
921
- startRow = Math.min(startRow, total);
922
- const endRow = Math.min(startRow + Math.ceil(viewportH / rowH) + (BUFFER_ROWS * 2), total);
923
-
924
- const colSpan = this.ths.length || 1;
925
- const topH = startRow * rowH;
926
- const bottomH = (total - endRow) * rowH;
927
-
928
- const frag = document.createDocumentFragment();
929
-
930
- if (topH > 0) {
931
- const topSpacer = document.createElement('tr');
932
- topSpacer.className = 'ln-table__spacer';
933
- topSpacer.setAttribute('aria-hidden', 'true');
934
- const topTd = document.createElement('td');
935
- topTd.setAttribute('colspan', colSpan);
936
- topTd.style.height = topH + 'px';
937
- topSpacer.appendChild(topTd);
938
- frag.appendChild(topSpacer);
939
- }
940
-
941
- for (let i = startRow; i < endRow; i++) {
942
- if (this._cache.has(i)) {
943
- const tr = this._buildRow(this._cache.get(i));
944
- if (tr) frag.appendChild(tr);
945
- } else {
946
- frag.appendChild(this._buildPlaceholderRow());
947
- }
948
- }
949
-
950
- if (bottomH > 0) {
951
- const bottomSpacer = document.createElement('tr');
952
- bottomSpacer.className = 'ln-table__spacer';
953
- bottomSpacer.setAttribute('aria-hidden', 'true');
954
- const bottomTd = document.createElement('td');
955
- bottomTd.setAttribute('colspan', colSpan);
956
- bottomTd.style.height = bottomH + 'px';
957
- bottomSpacer.appendChild(bottomTd);
958
- frag.appendChild(bottomSpacer);
959
- }
960
-
961
- const scrollState = _saveScroll(this);
962
- this.tbody.textContent = '';
963
- this.tbody.appendChild(frag);
964
- _restoreScroll(scrollState);
965
-
966
- // Select-all is disabled in windowed mode (D4) — no _updateSelectAll() call.
967
- this._vStart = startRow;
968
- this._vEnd = endRow;
969
-
970
- this._cache.ensure(startRow, endRow);
971
- };
972
-
973
- // ─── Empty state ───────────────────────────────────────────
974
-
975
- _component.prototype._showEmptyState = function () {
976
- const colSpan = this.ths.length || 1;
977
- this.tbody.textContent = '';
978
-
979
- let clone = null;
980
-
981
- if (this.isDataDriven) {
982
- const total = this._lastTotal != null ? this._lastTotal : this._data.length;
983
- const filtered = this.visibleCount;
984
- const isFiltered = (this.currentSearch || Object.keys(this.currentFilters).length > 0) && (filtered < total || filtered === 0);
985
-
986
- const templateName = isFiltered ? (this.name + '-empty-filtered') : (this.name + '-empty');
987
- clone = cloneTemplateScoped(this.dom, templateName, 'ln-table');
988
-
989
- if (!clone) {
990
- const genericTpl = this.dom.querySelector('template[data-ln-table-empty]');
991
- if (genericTpl) {
992
- const whenVal = isFiltered ? 'search' : 'initial';
993
- const subEl = genericTpl.content.querySelector('[data-ln-table-empty-when="' + whenVal + '"]')
994
- || genericTpl.content.firstElementChild;
995
- if (subEl) {
996
- clone = document.importNode(subEl, true);
997
- }
998
- }
999
- }
1000
-
1001
- if (clone) {
1002
- if (clone.tagName === 'TR') {
1003
- this.tbody.appendChild(clone);
1004
- } else {
1005
- const td = document.createElement('td');
1006
- td.setAttribute('colspan', String(colSpan));
1007
- td.appendChild(clone);
1008
- const tr = document.createElement('tr');
1009
- tr.className = 'ln-table__empty';
1010
- tr.appendChild(td);
1011
- this.tbody.appendChild(tr);
1012
- }
1013
- }
1014
- } else {
1015
- const tpl = this.dom.querySelector('template[' + EMPTY_TEMPLATE + ']');
1016
- const td = document.createElement('td');
1017
- td.setAttribute('colspan', String(colSpan));
1018
- if (tpl) td.appendChild(document.importNode(tpl.content, true));
1019
-
1020
- const tr = document.createElement('tr');
1021
- tr.className = 'ln-table__empty';
1022
- tr.appendChild(td);
1023
- this.tbody.appendChild(tr);
1024
- }
1025
-
1026
- dispatch(this.dom, 'ln-table:empty', {
1027
- term: this.isDataDriven ? (this.currentSearch || '') : this._searchTerm,
1028
- total: this.isDataDriven ? (this._lastTotal != null ? this._lastTotal : this._data.length) : this._data.length
1029
- });
1030
- };
1031
-
1032
- // ─── Templating / Row helpers ──────────────────────────────
1033
-
1034
- _component.prototype._fillRow = function (tr, record) {
1035
- fillTemplate(tr, record);
1036
-
1037
- const cellAttrs = tr.querySelectorAll('[data-ln-table-cell-attr]');
1038
- for (let i = 0; i < cellAttrs.length; i++) {
1039
- const el = cellAttrs[i];
1040
- const pairs = el.getAttribute('data-ln-table-cell-attr').split(',');
1041
- for (let j = 0; j < pairs.length; j++) {
1042
- const parts = pairs[j].trim().split(':');
1043
- if (parts.length !== 2) continue;
1044
- const field = parts[0].trim();
1045
- const attr = parts[1].trim();
1046
- if (record[field] != null) {
1047
- el.setAttribute(attr, record[field]);
1048
- }
1049
- }
1050
- }
1051
- };
1052
-
1053
- _component.prototype._buildRow = function (record) {
1054
- const clone = cloneTemplateScoped(this.dom, this.name + '-row', 'ln-table');
1055
- if (!clone) return null;
1056
-
1057
- const tr = clone.querySelector('[data-ln-table-row]') || clone.firstElementChild;
1058
- if (!tr) return null;
1059
-
1060
- this._fillRow(tr, record);
1061
-
1062
- tr._lnRecord = record;
1063
- if (record.id != null) {
1064
- tr.setAttribute('data-ln-table-row-id', record.id);
1065
- }
1066
-
1067
- if (this._selectable && record.id != null && this.selectedIds.has(String(record.id))) {
1068
- tr.classList.add('ln-row-selected');
1069
- const rowCb = tr.querySelector('[data-ln-table-row-select]');
1070
- if (rowCb) rowCb.checked = true;
1071
- }
1072
-
1073
- return tr;
1074
- };
1075
-
1076
- _component.prototype._requestData = function () {
1077
- if (this._windowed) {
1078
- this.dom.classList.add('ln-table--loading');
1079
- this._cache.invalidate({
1080
- sort: this.currentSort,
1081
- filters: this.currentFilters,
1082
- search: this.currentSearch
1083
- });
1084
- return;
1085
- }
1086
- requestData(this, 'ln-table:request-data', 'table');
1087
- };
1088
-
1089
- // ─── Windowed Mode — enter/exit/seed (live toggle) ──────────
1090
-
1091
- _component.prototype._enterWindowedMode = function () {
1092
- const self = this;
1093
- const dom = this.dom;
1094
- const winAttr = parseInt(dom.getAttribute('data-ln-table-window'), 10);
1095
- const pageAttr = parseInt(dom.getAttribute('data-ln-table-window-page'), 10);
1096
- const threshAttr = parseInt(dom.getAttribute('data-ln-table-window-threshold'), 10);
1097
-
1098
- // Cache change → sync totals off the cache, re-render (pull), notify.
1099
- this._onCacheChange = function () {
1100
- if (!self._windowed || !self._cache) return;
1101
- self.totalCount = self._cache.grandTotal;
1102
- self.visibleCount = self._cache.logicalTotal;
1103
- self._lastTotal = self._cache.grandTotal;
1104
- self.isLoaded = true;
1105
- self._vStart = -1;
1106
- self._vEnd = -1;
1107
- self._render();
1108
- self._updateFooter();
1109
- dispatch(dom, 'ln-table:rendered', {
1110
- table: self.name,
1111
- total: self.totalCount,
1112
- visible: self.visibleCount
1113
- });
1114
- };
1115
- this._renderBatch = createBatcher(this._onCacheChange);
1116
-
1117
- this._cache = createWindowCache({
1118
- windowSize: winAttr > 0 ? winAttr : WINDOW_DEFAULT,
1119
- pageSize: pageAttr > 0 ? pageAttr : WINDOW_PAGE,
1120
- threshold: threshAttr >= 0 ? threshAttr : 25,
1121
- fetchDebounce: FETCH_DEBOUNCE,
1122
- requestPage: function (query, offset, limit) {
1123
- dispatch(dom, 'ln-table:request-data', {
1124
- table: self.name,
1125
- sort: query.sort,
1126
- filters: query.filters,
1127
- search: query.search,
1128
- offset: offset,
1129
- limit: limit,
1130
- queryGen: self._cache.queryGen
1131
- });
1132
- },
1133
- onChange: this._renderBatch
1134
- });
1135
-
1136
- this._windowed = true;
1137
-
1138
- // D4 — windowed tables cannot select rows they have never fetched
1139
- if (this._selectable && this._selectAllCheckbox) {
1140
- this._selectAllCheckbox.classList.add('hidden');
1141
- }
1142
- };
1143
-
1144
- _component.prototype._kickWindowInitial = function () {
1145
- if (this._data.length > 0) {
1146
- // SSR-seeded / warm-seeded: page 0 is already resident, the grand
1147
- // total is declared in markup — no initial fetch needed. No
1148
- // queryGen on the seed — it must never be dropped as stale.
1149
- let declaredTotal = parseInt(this.dom.getAttribute('data-ln-table-count'), 10);
1150
- if (isNaN(declaredTotal) && this._totalSpan) {
1151
- const spanText = this._totalSpan.textContent.replace(/[^\d]/g, '');
1152
- if (spanText) declaredTotal = parseInt(spanText, 10);
1153
- }
1154
- const seedTotal = (declaredTotal > 0) ? declaredTotal : this._data.length;
1155
- this._cache.ingest({
1156
- data: this._data,
1157
- offset: 0,
1158
- total: seedTotal,
1159
- filtered: seedTotal
1160
- });
1161
- } else {
1162
- this.dom.classList.add('ln-table--loading');
1163
- this._cache.requestInitial({
1164
- sort: this.currentSort,
1165
- filters: this.currentFilters,
1166
- search: this.currentSearch
1167
- });
1168
- }
1169
- };
1170
-
1171
- _component.prototype._exitWindowedMode = function () {
1172
- this._disableVirtualScroll();
1173
- if (this._cache) this._cache.destroy();
1174
- this._cache = null;
1175
- this._windowed = false;
1176
- this._renderBatch = null;
1177
- this._onCacheChange = null;
1178
- if (this._selectAllCheckbox) {
1179
- this._selectAllCheckbox.classList.remove('hidden');
1180
- }
1181
- this._rowHeight = 0;
1182
- this._vStart = -1;
1183
- this._vEnd = -1;
1184
- this._data = [];
1185
- this._filteredData = [];
1186
- this.dom.classList.add('ln-table--loading');
1187
- this._requestData();
1188
- };
1189
-
1190
- // ─── Selection Helpers ─────────────────────────────────────
1191
-
1192
- _component.prototype._updateSelectAll = function () {
1193
- if (!this._selectAllCheckbox || !this.tbody) return;
1194
- const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1195
- let allSelected = rows.length > 0;
1196
- for (let i = 0; i < rows.length; i++) {
1197
- const id = rows[i].getAttribute('data-ln-table-row-id');
1198
- if (id != null && !this.selectedIds.has(id)) {
1199
- allSelected = false;
1200
- break;
1201
- }
1202
- }
1203
- this._selectAllCheckbox.checked = allSelected;
1204
- };
1205
-
1206
- // SSR re-render replays cached row HTML — reapply live selection state.
1207
- _component.prototype._restoreSelection = function () {
1208
- if (!this.tbody) return;
1209
- const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1210
- for (let i = 0; i < rows.length; i++) {
1211
- const id = rows[i].getAttribute('data-ln-table-row-id');
1212
- const selected = id != null && this.selectedIds.has(id);
1213
- rows[i].classList.toggle('ln-row-selected', selected);
1214
- const cb = rows[i].querySelector('[data-ln-table-row-select]');
1215
- if (cb) cb.checked = selected;
1216
- }
1217
- this._updateSelectAll();
1218
- };
1219
-
1220
- Object.defineProperty(_component.prototype, 'selectedCount', {
1221
- get: function () { return this.selectedIds.size; },
1222
- set: function () { /* computed from selectedIds */ }
1223
- });
1224
-
1225
- _component.prototype._enableSelection = function () {
1226
- if (this._selectableActive) return;
1227
- this._selectableActive = true;
1228
-
1229
- const self = this;
1230
- this._onSelectionChange = function (e) {
1231
- const checkbox = e.target.closest('[data-ln-table-row-select]');
1232
- if (!checkbox) return;
1233
- const tr = checkbox.closest('[data-ln-table-row]');
1234
- if (!tr) return;
1235
- const id = tr.getAttribute('data-ln-table-row-id');
1236
- if (id == null) return;
1237
-
1238
- if (checkbox.checked) {
1239
- self.selectedIds.add(id);
1240
- tr.classList.add('ln-row-selected');
1241
- } else {
1242
- self.selectedIds.delete(id);
1243
- tr.classList.remove('ln-row-selected');
1244
- }
1245
-
1246
- self.selectedCount = self.selectedIds.size;
1247
- self._updateSelectAll();
1248
- self._updateFooter();
1249
-
1250
- dispatch(self.dom, 'ln-table:select', {
1251
- table: self.name,
1252
- selectedIds: self.selectedIds,
1253
- count: self.selectedCount
1254
- });
1255
- };
1256
- if (this.tbody) this.tbody.addEventListener('change', this._onSelectionChange);
1257
-
1258
- this._selectAllCheckbox = this.dom.querySelector('[data-ln-table-col-select] input[type="checkbox"]')
1259
- || this.dom.querySelector('[data-ln-table-col-select]');
1260
- if (this._selectAllCheckbox && this._selectAllCheckbox.tagName === 'TH') {
1261
- const cb = document.createElement('input');
1262
- cb.type = 'checkbox';
1263
- const dictEl = self.dom.querySelector('[data-ln-table-dict="select-all"]');
1264
- const label = self.dom.getAttribute('data-ln-table-select-all-label') || (dictEl ? dictEl.textContent.trim() : null) || 'Select all';
1265
- cb.setAttribute('aria-label', label);
1266
- this._selectAllCheckbox.appendChild(cb);
1267
- this._selectAllCheckbox = cb;
1268
- }
1269
-
1270
- if (this._selectAllCheckbox) {
1271
- this._onSelectAll = function () {
1272
- const checked = self._selectAllCheckbox.checked;
1273
- const rows = self.tbody ? self.tbody.querySelectorAll('[data-ln-table-row]') : [];
1274
-
1275
- for (let i = 0; i < rows.length; i++) {
1276
- const id = rows[i].getAttribute('data-ln-table-row-id');
1277
- const rowCb = rows[i].querySelector('[data-ln-table-row-select]');
1278
- if (id == null) continue;
1279
-
1280
- if (checked) {
1281
- self.selectedIds.add(id);
1282
- rows[i].classList.add('ln-row-selected');
1283
- } else {
1284
- self.selectedIds.delete(id);
1285
- rows[i].classList.remove('ln-row-selected');
1286
- }
1287
- if (rowCb) rowCb.checked = checked;
1288
- }
1289
-
1290
- self.selectedCount = self.selectedIds.size;
1291
- dispatch(self.dom, 'ln-table:select-all', {
1292
- table: self.name,
1293
- selected: checked
1294
- });
1295
- dispatch(self.dom, 'ln-table:select', {
1296
- table: self.name,
1297
- selectedIds: self.selectedIds,
1298
- count: self.selectedCount
1299
- });
1300
- self._updateFooter();
1301
- };
1302
- this._selectAllCheckbox.addEventListener('change', this._onSelectAll);
1303
- }
1304
-
1305
- if (this.tbody) {
1306
- const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1307
- for (let i = 0; i < rows.length; i++) {
1308
- const cb = rows[i].querySelector('[data-ln-table-row-select]');
1309
- const id = rows[i].getAttribute('data-ln-table-row-id');
1310
- if (cb && cb.checked && id != null) {
1311
- this.selectedIds.add(id);
1312
- rows[i].classList.add('ln-row-selected');
1313
- }
1314
- }
1315
- this.selectedCount = this.selectedIds.size;
1316
- if (this.selectedCount > 0) this._updateSelectAll();
1317
- }
1318
- };
1319
-
1320
- _component.prototype._disableSelection = function () {
1321
- if (!this._selectableActive) return;
1322
- this._selectableActive = false;
1323
-
1324
- if (this.tbody && this._onSelectionChange) {
1325
- this.tbody.removeEventListener('change', this._onSelectionChange);
1326
- }
1327
- if (this._selectAllCheckbox && this._onSelectAll) {
1328
- this._selectAllCheckbox.removeEventListener('change', this._onSelectAll);
1329
- }
1330
-
1331
- const th = this.dom.querySelector('[data-ln-table-col-select]');
1332
- if (th) {
1333
- const cb = th.querySelector('input[type="checkbox"]');
1334
- if (cb) {
1335
- cb.remove();
1336
- }
1337
- }
1338
- this._selectAllCheckbox = null;
1339
-
1340
- this.selectedIds.clear();
1341
- this.selectedCount = 0;
1342
-
1343
- if (this.tbody) {
1344
- const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1345
- for (let i = 0; i < rows.length; i++) {
1346
- rows[i].classList.remove('ln-row-selected');
1347
- const cb = rows[i].querySelector('[data-ln-table-row-select]');
1348
- if (cb) cb.checked = false;
1349
- }
1350
- }
1351
-
1352
- this._updateFooter();
1353
- };
1354
-
1355
- // ─── Footer Helpers ────────────────────────────────────────
1356
-
1357
- _component.prototype._updateFooter = function () {
1358
- let total = 0;
1359
- let filtered = 0;
1360
-
1361
- if (this.isDataDriven) {
1362
- total = this._lastTotal != null ? this._lastTotal : this._data.length;
1363
- filtered = this.visibleCount;
1364
- } else {
1365
- total = this._data.length;
1366
- filtered = this._filteredData.length;
1367
- }
1368
-
1369
- const isFiltered = filtered < total;
1370
-
1371
- if (this._totalSpan) {
1372
- this._totalSpan.textContent = _formatNum(total, this.dom);
1373
- }
1374
-
1375
- if (this._filteredSpan) {
1376
- this._filteredSpan.textContent = isFiltered ? _formatNum(filtered, this.dom) : '';
1377
- }
1378
-
1379
- if (this._filteredWrap) {
1380
- this._filteredWrap.classList.toggle('hidden', !isFiltered);
1381
- }
1382
-
1383
- if (this._selectedSpan) {
1384
- const count = this.selectedIds ? this.selectedIds.size : 0;
1385
- this._selectedSpan.textContent = count > 0 ? _formatNum(count, this.dom) : '';
1386
- if (this._selectedWrap) {
1387
- this._selectedWrap.classList.toggle('hidden', count === 0);
1388
- }
1389
- }
1390
- };
1391
-
1392
- // ─── Keyboard Focus Helpers ────────────────────────────────
1393
-
1394
- _component.prototype._focusRow = function (rows) {
1395
- for (let i = 0; i < rows.length; i++) {
1396
- rows[i].classList.remove('ln-row-focused');
1397
- rows[i].removeAttribute('tabindex');
1398
- }
1399
-
1400
- if (this._focusedRowIndex >= 0 && this._focusedRowIndex < rows.length) {
1401
- const row = rows[this._focusedRowIndex];
1402
- row.classList.add('ln-row-focused');
1403
- row.setAttribute('tabindex', '0');
1404
- row.focus();
1405
- row.scrollIntoView({ block: 'nearest' });
1406
- }
1407
- };
1408
-
1409
- // ─── Destroy ───────────────────────────────────────────────
1410
-
1411
- _component.prototype.destroy = function () {
1412
- if (!this.dom[DOM_ATTRIBUTE]) return;
1413
- this._disableVirtualScroll();
1414
-
1415
- this.dom.removeEventListener('ln-table:set-search', this._onSetSearch);
1416
- this.dom.removeEventListener('ln-table:set-filter', this._onSetFilter);
1417
- this.dom.removeEventListener('ln-table:request-clear-filters', this._onRequestClearFilters);
1418
-
1419
- if (this.isDataDriven) {
1420
- this.dom.removeEventListener('ln-table:set-data', this._onSetData);
1421
- this.dom.removeEventListener('ln-table:set-loading', this._onSetLoading);
1422
- this.dom.removeEventListener('ln-table:page-failed', this._onPageFailed);
1423
- this.dom.removeEventListener('ln-table:request-revalidate', this._onRequestRevalidate);
1424
- this.dom.removeEventListener('ln-sort:change', this._onSort);
1425
- document.removeEventListener('keydown', this._onKeydown);
1426
- if (this.tbody) {
1427
- this.tbody.removeEventListener('click', this._onRowClick);
1428
- this.tbody.removeEventListener('click', this._onRowAction);
1429
- }
1430
- if (this._cache) this._cache.destroy();
1431
- } else {
1432
- if (this._emptyTbodyObserver) {
1433
- this._emptyTbodyObserver.disconnect();
1434
- this._emptyTbodyObserver = null;
1435
- }
1436
- this.dom.removeEventListener('ln-sort:change', this._onSort);
1437
- this.dom.removeEventListener('ln-search:change', this._onSearchChange);
1438
- }
1439
-
1440
- if (this._onSelectionChange && this.tbody) this.tbody.removeEventListener('change', this._onSelectionChange);
1441
- if (this._selectAllCheckbox && this._onSelectAll) this._selectAllCheckbox.removeEventListener('change', this._onSelectAll);
1442
-
1443
- if (this._colgroup) {
1444
- this._colgroup.remove();
1445
- this._colgroup = null;
1446
- }
1447
- if (this.table) this.table.style.tableLayout = '';
1448
- this._data = [];
1449
- this._filteredData = [];
1450
- delete this.dom[DOM_ATTRIBUTE];
1451
- };
1452
-
1453
- // ─── Init ──────────────────────────────────────────────────
1454
-
1455
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-table', {
1456
- extraAttributes: [
1457
- 'data-ln-table-window',
1458
- 'data-ln-table-window-page',
1459
- 'data-ln-table-window-threshold',
1460
- 'data-ln-table-count'
1461
- ],
1462
- onAttributeChange: function (el, attrName) {
1463
- const inst = el[DOM_ATTRIBUTE];
1464
- if (!inst || !inst.isDataDriven) return;
1465
-
1466
- if (attrName === 'data-ln-table-window') {
1467
- const present = el.hasAttribute('data-ln-table-window');
1468
- if (present && !inst._windowed) {
1469
- inst._enterWindowedMode();
1470
- inst._kickWindowInitial();
1471
- } else if (!present && inst._windowed) {
1472
- inst._exitWindowedMode();
1473
- } else if (present && inst._windowed) {
1474
- const v = parseInt(el.getAttribute('data-ln-table-window'), 10);
1475
- if (v > 0) inst._cache.configure({ windowSize: v });
1476
- }
1477
- return;
1478
- }
1479
-
1480
- if (!inst._windowed || !inst._cache) return;
1481
- if (attrName === 'data-ln-table-window-page') {
1482
- const v = parseInt(el.getAttribute('data-ln-table-window-page'), 10);
1483
- if (v > 0) inst._cache.configure({ pageSize: v });
1484
- } else if (attrName === 'data-ln-table-window-threshold') {
1485
- const v = parseInt(el.getAttribute('data-ln-table-window-threshold'), 10);
1486
- if (v >= 0) inst._cache.configure({ threshold: v });
1487
- } else if (attrName === 'data-ln-table-count') {
1488
- const v = parseInt(el.getAttribute('data-ln-table-count'), 10);
1489
- if (v >= 0) inst._cache.setGrandTotal(v);
1490
- }
1491
- }
1492
- });
1493
- })();
1
+ import { cloneTemplateScoped, createBatcher, createWindowCache, dispatch, fill, fillTemplate, getLocale, readValue, registerComponent, requestData, attrBool, attrStr, attrSpec, defineAttrs } from '../../ln-core';
2
+ import { calculateSelectionState, calculateVirtualWindow, toggleRowSelection, toggleSelectAll } from './table-model.js';
3
+
4
+ (function () {
5
+ const DOM_SELECTOR = 'data-ln-table';
6
+ const DOM_ATTRIBUTE = 'lnTable';
7
+ const EMPTY_TEMPLATE = 'data-ln-table-empty';
8
+ // Tuning constant — duplicated in ln-data-table for component independence
9
+ const VIRTUAL_THRESHOLD = 200;
10
+ const BUFFER_ROWS = 15;
11
+ const WINDOW_DEFAULT = 1000;
12
+ const WINDOW_PAGE = 200;
13
+ const FETCH_DEBOUNCE = 120;
14
+
15
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
16
+
17
+ // ─── Attribute Contract (SSOT) ──────────────────────────
18
+ function _applyWindow(inst, val) {
19
+ if (!inst || !inst.isDataDriven) return;
20
+ const present = inst.dom.hasAttribute('data-ln-table-window');
21
+ if (present && !inst._windowed) {
22
+ inst._enterWindowedMode();
23
+ inst._kickWindowInitial();
24
+ } else if (!present && inst._windowed) {
25
+ inst._exitWindowedMode();
26
+ } else if (present && inst._windowed) {
27
+ const v = parseInt(val, 10);
28
+ if (v > 0) inst._cache.configure({ windowSize: v });
29
+ }
30
+ }
31
+
32
+ function _applyWindowPage(inst, val) {
33
+ if (!inst || !inst.isDataDriven || !inst._windowed || !inst._cache) return;
34
+ const v = parseInt(val, 10);
35
+ if (v > 0) inst._cache.configure({ pageSize: v });
36
+ }
37
+
38
+ function _applyWindowThreshold(inst, val) {
39
+ if (!inst || !inst.isDataDriven || !inst._windowed || !inst._cache) return;
40
+ const v = parseInt(val, 10);
41
+ if (v >= 0) inst._cache.configure({ threshold: v });
42
+ }
43
+
44
+ function _applyCount(inst, val) {
45
+ if (!inst || !inst.isDataDriven || !inst._windowed || !inst._cache) return;
46
+ const v = parseInt(val, 10);
47
+ if (v >= 0) inst._cache.setGrandTotal(v);
48
+ }
49
+
50
+ const ATTRIBUTES = {
51
+ 'data-ln-table': { prop: 'name', type: 'string', read: attrStr, fallback: '', description: 'Table instance name or identifier' },
52
+ 'data-ln-table-source': { prop: 'source', type: 'string', read: attrStr, fallback: '', description: 'Source store or coordinator addressing' },
53
+ 'data-ln-table-selectable': { prop: '_selectable', type: 'boolean', read: attrBool, description: 'Enables row selection check controls' },
54
+ 'data-ln-table-window': { type: 'integer', fallback: WINDOW_DEFAULT, min: 10, effect: _applyWindow, description: 'Virtual scrolling window size in rows' },
55
+ 'data-ln-table-window-page': { type: 'integer', fallback: WINDOW_PAGE, min: 5, effect: _applyWindowPage, description: 'Virtual scrolling slice page size' },
56
+ 'data-ln-table-window-threshold': { type: 'integer', fallback: 50, min: 0, effect: _applyWindowThreshold, description: 'Scroll threshold margin in pixels to trigger page fetch' },
57
+ 'data-ln-table-count': { type: 'integer', min: 0, effect: _applyCount, description: 'Total record count override for virtual scrollbar calculation' },
58
+ 'data-ln-table-row': { type: 'marker', description: 'Table row template element' },
59
+ 'data-ln-table-row-id': { type: 'string', description: 'Record identifier on row element' },
60
+ 'data-ln-table-row-action': { type: 'trigger', description: 'Action trigger inside a table row' },
61
+ 'data-ln-table-row-select': { type: 'trigger', description: 'Row selection checkbox trigger' },
62
+ 'data-ln-table-col': { type: 'string', description: 'Column header identifier or field mapping' },
63
+ 'data-ln-table-col-select': { type: 'trigger', description: 'Header select-all checkbox trigger' },
64
+ 'data-ln-table-cell-attr': { type: 'string', description: 'Cell attribute template mapping' },
65
+ 'data-ln-table-empty': { type: 'marker', description: 'Container for table empty state' },
66
+ 'data-ln-table-select-all-label': { type: 'string', description: 'Accessibility label for select-all header trigger' }
67
+ };
68
+
69
+ const ATTR_SPEC = attrSpec(ATTRIBUTES);
70
+
71
+ // Singleton — same lang for all table instances on the page
72
+ const _collator = typeof Intl !== 'undefined'
73
+ ? new Intl.Collator(document.documentElement.lang || undefined, { sensitivity: 'base' })
74
+ : null;
75
+
76
+ function _formatNum(n, dom) {
77
+ if (n == null || isNaN(n)) return '';
78
+ try {
79
+ return new Intl.NumberFormat(getLocale(dom)).format(n);
80
+ } catch (e) {
81
+ return String(n);
82
+ }
83
+ }
84
+
85
+ function _findScrollContainer(el) {
86
+ let p = el.parentElement;
87
+ while (p && p !== document.body && p !== document.documentElement) {
88
+ const cs = getComputedStyle(p);
89
+ const oy = cs.overflowY;
90
+ if (oy === 'auto' || oy === 'scroll') return p;
91
+ p = p.parentElement;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ // ─── Component ─────────────────────────────────────────────
97
+
98
+ function _component(dom) {
99
+ this.dom = dom;
100
+ defineAttrs(this, dom, ATTR_SPEC);
101
+ this.table = dom.querySelector('table');
102
+ this.tbody = dom.querySelector('[data-ln-table-body]') || dom.querySelector('tbody');
103
+ this.thead = dom.querySelector('thead');
104
+ // Scope to the column-headers row (last <tr> in thead).
105
+ const colHeaderRow = this.thead ? this.thead.querySelector('tr:last-child') : null;
106
+ this.ths = colHeaderRow ? Array.from(colHeaderRow.querySelectorAll('th')) : [];
107
+
108
+ // Footer elements — both modes
109
+ this._totalSpan = dom.querySelector('[data-ln-table-total]');
110
+ this._filteredSpan = dom.querySelector('[data-ln-table-filtered]');
111
+ if (this._filteredSpan) {
112
+ this._filteredWrap = this._filteredSpan.parentElement !== dom
113
+ ? this._filteredSpan.parentElement
114
+ : null;
115
+ }
116
+ this._selectedSpan = dom.querySelector('[data-ln-table-selected]');
117
+ if (this._selectedSpan) {
118
+ this._selectedWrap = this._selectedSpan.parentElement !== dom
119
+ ? this._selectedSpan.parentElement
120
+ : null;
121
+ }
122
+
123
+ this.isDataDriven = dom.hasAttribute('data-ln-table-source');
124
+
125
+ this._data = [];
126
+ this._filteredData = [];
127
+ this._searchTerm = '';
128
+ this._sortCol = -1;
129
+ this._sortDir = null;
130
+ this._columnFilters = {};
131
+ this.selectedIds = new Set();
132
+
133
+ // Virtual scroll state
134
+ this._virtual = false;
135
+ this._rowHeight = 0;
136
+ this._vStart = -1;
137
+ this._vEnd = -1;
138
+ this._rafId = null;
139
+ this._scrollHandler = null;
140
+ this._scrollContainer = null;
141
+ this._colgroup = null;
142
+
143
+ const self = this;
144
+
145
+ // --- Layer 1 Command / Request Event Handlers ---
146
+
147
+ this._onSetSearch = function (e) {
148
+ const term = (e.detail && e.detail.query != null ? e.detail.query : (e.detail && e.detail.term != null ? e.detail.term : '')).trim();
149
+ if (self.isDataDriven) {
150
+ self.currentSearch = term;
151
+ dispatch(dom, 'ln-table:search', {
152
+ table: self.name,
153
+ query: self.currentSearch
154
+ });
155
+ self._requestData();
156
+ } else {
157
+ self._searchTerm = term.toLowerCase();
158
+ self._applyFilterAndSort();
159
+ self._vStart = -1;
160
+ self._vEnd = -1;
161
+ self._render();
162
+ self._updateFooter();
163
+ dispatch(dom, 'ln-table:filter', {
164
+ term: self._searchTerm,
165
+ matched: self._filteredData.length,
166
+ total: self._data.length
167
+ });
168
+ }
169
+ };
170
+ dom.addEventListener('ln-table:set-search', this._onSetSearch);
171
+
172
+ this._onSearchChange = function (e) {
173
+ e.preventDefault();
174
+ self._onSetSearch(e);
175
+ };
176
+ dom.addEventListener('ln-search:change', this._onSearchChange);
177
+
178
+ this._onRequestClearFilters = function () {
179
+ if (self.isDataDriven) {
180
+ self.currentFilters = {};
181
+ self.currentSearch = '';
182
+ dispatch(dom, 'ln-table:clear-filters', { table: self.name });
183
+ self._requestData();
184
+ } else {
185
+ self._searchTerm = '';
186
+ self._columnFilters = {};
187
+ self._applyFilterAndSort();
188
+ self._vStart = -1;
189
+ self._vEnd = -1;
190
+ self._render();
191
+ self._updateFooter();
192
+ dispatch(dom, 'ln-table:filter', {
193
+ term: '',
194
+ matched: self._filteredData.length,
195
+ total: self._data.length
196
+ });
197
+ }
198
+ };
199
+ dom.addEventListener('ln-table:request-clear-filters', this._onRequestClearFilters);
200
+
201
+ // --- Selection (both modes) ---
202
+ this._selectableActive = false;
203
+ if (this._selectable) {
204
+ this._enableSelection();
205
+ }
206
+
207
+ if (this.isDataDriven) {
208
+ this.isLoaded = false;
209
+ this.totalCount = 0;
210
+ this.visibleCount = 0;
211
+ this.currentSort = null;
212
+ this.currentFilters = {};
213
+ this.currentSearch = '';
214
+
215
+ this._lastTotal = 0;
216
+ this._lastFiltered = 0;
217
+ this._hasInitialSeed = false;
218
+
219
+ // Windowed (sliding-window) server-side virtualization — opt-in
220
+ this._windowed = false;
221
+ this._cache = null;
222
+ if (this.isDataDriven && dom.hasAttribute('data-ln-table-window')) this._enterWindowedMode();
223
+
224
+ // --- Event listeners ---
225
+ this._onSetData = function (e) {
226
+ const detail = e.detail || {};
227
+ const newData = detail.data || [];
228
+ const newTotal = detail.total != null ? detail.total : newData.length;
229
+
230
+ // Protect initial SSR seed rows: do not overwrite pre-rendered HTML rows with empty un-synced store payloads before initial API response arrives
231
+ if (self._hasInitialSeed && !self.isLoaded && newData.length === 0 && newTotal === 0) {
232
+ return;
233
+ }
234
+
235
+ if (self._windowed) {
236
+ // Only an accepted, authoritative page ends the refresh. A declined
237
+ // one means the source has not resolved the new query yet; a
238
+ // provisional one is the store's own answer, rendered while the
239
+ // server query is still out.
240
+ if (self._cache.ingest(detail) && !detail.provisional) dom.classList.remove('ln-table--loading');
241
+ return;
242
+ }
243
+
244
+ self._data = newData;
245
+ self._lastTotal = newTotal;
246
+ self._lastFiltered = detail.filtered != null ? detail.filtered : self._data.length;
247
+
248
+ self.totalCount = self._lastTotal;
249
+ self.visibleCount = self._lastFiltered;
250
+ self.isLoaded = true;
251
+ self._hasInitialSeed = false;
252
+
253
+ dom.classList.remove('ln-table--loading');
254
+
255
+ self._vStart = -1;
256
+ self._vEnd = -1;
257
+
258
+ self._applyFilterAndSort();
259
+ self._render();
260
+ self._updateFooter();
261
+
262
+ dispatch(dom, 'ln-table:rendered', {
263
+ table: self.name,
264
+ total: self.totalCount,
265
+ visible: self.visibleCount
266
+ });
267
+ };
268
+ dom.addEventListener('ln-table:set-data', this._onSetData);
269
+
270
+ this._onSetLoading = function (e) {
271
+ const loading = e.detail && e.detail.loading;
272
+ dom.classList.toggle('ln-table--loading', !!loading);
273
+ if (loading) {
274
+ self.isLoaded = false;
275
+ }
276
+ };
277
+ dom.addEventListener('ln-table:set-loading', this._onSetLoading);
278
+
279
+ this._onPageFailed = function (e) {
280
+ if (!self._windowed || !self._cache) return;
281
+ self._cache.release(e.detail && e.detail.offset);
282
+ };
283
+ dom.addEventListener('ln-table:page-failed', this._onPageFailed);
284
+
285
+ this._onRequestRevalidate = function () {
286
+ if (!self._windowed || !self._cache) return;
287
+ self._cache.revalidate();
288
+ };
289
+ dom.addEventListener('ln-table:request-revalidate', this._onRequestRevalidate);
290
+
291
+ // The source owns the query, so a query change reaches the view as a
292
+ // restart order, not as query state — the window drops to page 0 and
293
+ // the composed query is resolved at serve time.
294
+ this._onRequestInvalidate = function () {
295
+ if (!self._windowed || !self._cache) return;
296
+ self._requestData();
297
+ };
298
+ dom.addEventListener('ln-table:request-invalidate', this._onRequestInvalidate);
299
+
300
+ // --- Sort ---
301
+ this._onSort = function (e) {
302
+ e.preventDefault();
303
+ self.currentSort = e.detail.direction === 'none' ? null : { field: e.detail.field, direction: e.detail.direction };
304
+ self._requestData();
305
+ };
306
+ dom.addEventListener('ln-sort:change', this._onSort);
307
+
308
+ // D4 — windowed tables cannot select rows they have never fetched
309
+ if (this._windowed && this._selectable && this._selectAllCheckbox) {
310
+ this._selectAllCheckbox.classList.add('hidden');
311
+ }
312
+
313
+ // --- Row Click & Actions ---
314
+ this._onRowClick = function (e) {
315
+ if (e.target.closest('[data-ln-table-row-select]')) return;
316
+ if (e.target.closest('[data-ln-table-row-action]')) return;
317
+ if (e.target.closest('a') || e.target.closest('button')) return;
318
+ if (e.ctrlKey || e.metaKey || e.button === 1) return;
319
+
320
+ const tr = e.target.closest('[data-ln-table-row]');
321
+ if (!tr) return;
322
+
323
+ const id = tr.getAttribute('data-ln-table-row-id');
324
+ const record = tr._lnRecord || {};
325
+
326
+ dispatch(dom, 'ln-table:row-click', {
327
+ table: self.name,
328
+ id: id,
329
+ record: record
330
+ });
331
+ };
332
+ if (this.tbody) this.tbody.addEventListener('click', this._onRowClick);
333
+
334
+ this._onRowAction = function (e) {
335
+ const btn = e.target.closest('[data-ln-table-row-action]');
336
+ if (!btn) return;
337
+
338
+ const tr = btn.closest('[data-ln-table-row]');
339
+ if (!tr) return;
340
+
341
+ const action = btn.getAttribute('data-ln-table-row-action');
342
+ const id = tr.getAttribute('data-ln-table-row-id');
343
+ const record = tr._lnRecord || {};
344
+
345
+ dispatch(dom, 'ln-table:row-action', {
346
+ table: self.name,
347
+ id: id,
348
+ action: action,
349
+ record: record
350
+ });
351
+ };
352
+ if (this.tbody) this.tbody.addEventListener('click', this._onRowAction);
353
+
354
+
355
+
356
+ // Local hydration of initial SSR rows
357
+ if (this.tbody && this.tbody.rows.length > 0) {
358
+ this._parseRows();
359
+ }
360
+
361
+ // Initial request-data
362
+ if (this._windowed) {
363
+ this._kickWindowInitial();
364
+ } else {
365
+ dispatch(dom, 'ln-table:request-data', {
366
+ table: this.name,
367
+ sort: this.currentSort,
368
+ filters: this.currentFilters,
369
+ search: this.currentSearch
370
+ });
371
+ }
372
+
373
+ } else {
374
+ // SSR Mode
375
+ this._emptyTbodyObserver = null;
376
+
377
+ if (this.tbody && this.tbody.rows.length > 0) {
378
+ this._parseRows();
379
+ } else if (this.tbody) {
380
+ this._emptyTbodyObserver = new MutationObserver(function () {
381
+ if (self.tbody.rows.length > 0) {
382
+ self._emptyTbodyObserver.disconnect();
383
+ self._emptyTbodyObserver = null;
384
+ self._parseRows();
385
+ }
386
+ });
387
+ this._emptyTbodyObserver.observe(this.tbody, { childList: true });
388
+ }
389
+
390
+ this._onSort = function (e) {
391
+ e.preventDefault();
392
+ const direction = e.detail.direction === 'none' ? null : e.detail.direction;
393
+ self._sortCol = direction === null ? -1 : e.detail.column;
394
+ self._sortDir = direction;
395
+ self._applyFilterAndSort();
396
+ self._vStart = -1;
397
+ self._vEnd = -1;
398
+ self._render();
399
+ dispatch(dom, 'ln-table:sorted', {
400
+ column: e.detail.column,
401
+ direction: e.detail.direction,
402
+ matched: self._filteredData.length,
403
+ total: self._data.length
404
+ });
405
+ };
406
+ dom.addEventListener('ln-sort:change', this._onSort);
407
+
408
+ this._onFilterChange = function (e) {
409
+ e.preventDefault();
410
+ if (!e.detail) return;
411
+ const key = e.detail.key;
412
+ const values = e.detail.values || [];
413
+ if (!key) return;
414
+
415
+ if (values.length === 0) {
416
+ delete self._columnFilters[key];
417
+ } else {
418
+ const lower = [];
419
+ for (let i = 0; i < values.length; i++) {
420
+ lower.push(values[i].toLowerCase());
421
+ }
422
+ self._columnFilters[key] = lower;
423
+ }
424
+ self._applyFilterAndSort();
425
+ self._vStart = -1;
426
+ self._vEnd = -1;
427
+ self._render();
428
+ self._updateFooter();
429
+ dispatch(dom, 'ln-table:filter', {
430
+ term: self._searchTerm,
431
+ matched: self._filteredData.length,
432
+ total: self._data.length
433
+ });
434
+ };
435
+ dom.addEventListener('ln-filter:change', this._onFilterChange);
436
+ }
437
+
438
+ return this;
439
+ }
440
+
441
+ // ─── Parse rows into in-memory array ───────────────────────
442
+
443
+ _component.prototype._parseRows = function () {
444
+ const rows = this.tbody.rows;
445
+ const ths = this.ths;
446
+ this._data = [];
447
+
448
+ if (rows.length > 0) this._rowHeight = rows[0].offsetHeight || 40;
449
+ this._lockColumnWidths();
450
+
451
+ for (let i = 0; i < rows.length; i++) {
452
+ const tr = rows[i];
453
+ const values = [];
454
+ const rawTexts = [];
455
+ const searchParts = [];
456
+
457
+ for (let j = 0; j < tr.cells.length; j++) {
458
+ const td = tr.cells[j];
459
+ const text = td.textContent.trim();
460
+
461
+ values[j] = readValue(td);
462
+ rawTexts[j] = text.toLowerCase();
463
+
464
+ if (!td.querySelector('[data-ln-table-row-action]')) {
465
+ searchParts.push(text.toLowerCase());
466
+ }
467
+ }
468
+
469
+ let record = null;
470
+ if (this.isDataDriven) {
471
+ record = {};
472
+ const id = tr.getAttribute('data-ln-table-row-id');
473
+ if (id != null) record.id = id;
474
+
475
+ for (let j = 0; j < ths.length; j++) {
476
+ const field = ths[j].getAttribute('data-ln-table-col');
477
+ if (field) {
478
+ const cellIndex = j;
479
+ if (cellIndex < tr.cells.length) {
480
+ const td = tr.cells[cellIndex];
481
+ record[field] = readValue(td);
482
+ }
483
+ }
484
+ }
485
+ }
486
+
487
+ this._data.push({
488
+ values: values,
489
+ rawTexts: rawTexts,
490
+ html: tr.outerHTML,
491
+ searchText: searchParts.join(' '),
492
+ id: this.isDataDriven && record ? record.id : undefined,
493
+ ...record
494
+ });
495
+ }
496
+
497
+ this._filteredData = this._data.slice();
498
+ if (this._data.length > 0) {
499
+ this._hasInitialSeed = true;
500
+ }
501
+
502
+ if (this.isDataDriven) {
503
+ this._lastTotal = this._data.length;
504
+ this._lastFiltered = this._data.length;
505
+ this.totalCount = this._data.length;
506
+ this.visibleCount = this._data.length;
507
+ this._updateFooter();
508
+ }
509
+
510
+ this._render();
511
+
512
+ dispatch(this.dom, 'ln-table:ready', {
513
+ total: this._data.length
514
+ });
515
+ };
516
+
517
+ // ─── Filter + Sort ─────────────────────────────────────────
518
+
519
+ _component.prototype._applyFilterAndSort = function () {
520
+ this._filteredData = this._data ? this._data.slice() : [];
521
+ this.visibleCount = (this.isDataDriven && this._lastFiltered != null) ? this._lastFiltered : this._filteredData.length;
522
+ };
523
+
524
+ // ─── Column width locking ──────────────────────────────────
525
+
526
+ _component.prototype._lockColumnWidths = function () {
527
+ if (!this.table || !this.thead || this._colgroup) return;
528
+
529
+ const colgroup = document.createElement('colgroup');
530
+ this.ths.forEach(function (th) {
531
+ const col = document.createElement('col');
532
+ col.style.width = th.offsetWidth + 'px';
533
+ colgroup.appendChild(col);
534
+ });
535
+
536
+ this.table.insertBefore(colgroup, this.table.firstChild);
537
+ this.table.style.tableLayout = 'fixed';
538
+ this._colgroup = colgroup;
539
+ };
540
+
541
+ // ─── Render ────────────────────────────────────────────────
542
+
543
+ _component.prototype._render = function () {
544
+ if (!this.tbody) return;
545
+
546
+ if (this.isDataDriven) {
547
+ if (this._windowed) { this._renderWindowed(); return; }
548
+ const total = this._lastTotal;
549
+ const filtered = this.visibleCount;
550
+
551
+ if (total === 0) {
552
+ this._disableVirtualScroll();
553
+ this._showEmptyState();
554
+ return;
555
+ }
556
+
557
+ if (this._filteredData.length === 0 || filtered === 0) {
558
+ this._disableVirtualScroll();
559
+ this._showEmptyState();
560
+ return;
561
+ }
562
+
563
+ if (this._filteredData.length > VIRTUAL_THRESHOLD) {
564
+ this._enableVirtualScroll();
565
+ this._renderVirtual();
566
+ } else {
567
+ this._disableVirtualScroll();
568
+ this._renderAll();
569
+ }
570
+ } else {
571
+ const count = this._filteredData.length;
572
+
573
+ if (count === 0 && (this._searchTerm || Object.keys(this._columnFilters).length > 0)) {
574
+ this._disableVirtualScroll();
575
+ this._showEmptyState();
576
+ } else if (count > VIRTUAL_THRESHOLD) {
577
+ this._enableVirtualScroll();
578
+ this._renderVirtual();
579
+ } else {
580
+ this._disableVirtualScroll();
581
+ this._renderAll();
582
+ }
583
+ }
584
+ };
585
+
586
+ _component.prototype._renderAll = function () {
587
+ if (this.isDataDriven) {
588
+ const data = this._filteredData;
589
+ const frag = document.createDocumentFragment();
590
+
591
+ for (let i = 0; i < data.length; i++) {
592
+ const tr = this._buildRow(data[i]);
593
+ if (!tr) break;
594
+ frag.appendChild(tr);
595
+ }
596
+
597
+ this.tbody.replaceChildren(frag);
598
+
599
+ if (this._selectable) this._updateSelectAll();
600
+ } else {
601
+ const html = [];
602
+ const data = this._filteredData;
603
+ for (let i = 0; i < data.length; i++) html.push(data[i].html);
604
+
605
+ this.tbody.innerHTML = html.join('');
606
+
607
+ if (this._selectable) this._restoreSelection();
608
+ }
609
+ };
610
+
611
+ // ─── Virtual scroll ────────────────────────────────────────
612
+
613
+ _component.prototype._enableVirtualScroll = function () {
614
+ if (this._virtual) return;
615
+ this._virtual = true;
616
+ this._vStart = -1;
617
+ this._vEnd = -1;
618
+ const self = this;
619
+
620
+ if (!this._rowHeight) {
621
+ if (this.tbody && this.tbody.rows.length > 0) {
622
+ this._rowHeight = this.tbody.rows[0].offsetHeight || 40;
623
+ } else {
624
+ let tempRow = null;
625
+ if (this._windowed) {
626
+ const sample = this._cache ? this._cache.peek() : null;
627
+ tempRow = sample ? this._buildRow(sample) : this._buildPlaceholderRow();
628
+ } else if (this.isDataDriven && this._data.length > 0) {
629
+ tempRow = this._buildRow(this._data[0]);
630
+ }
631
+ if (tempRow && this.tbody) {
632
+ this.tbody.appendChild(tempRow);
633
+ this._rowHeight = tempRow.offsetHeight || 40;
634
+ tempRow.remove();
635
+ }
636
+ }
637
+ }
638
+
639
+ if (this.isDataDriven) {
640
+ this._scrollContainer = _findScrollContainer(this.dom);
641
+ } else {
642
+ this._scrollContainer = null;
643
+ }
644
+ const scrollTarget = this._scrollContainer || window;
645
+
646
+ this._scrollHandler = function () {
647
+ if (self._rafId) return;
648
+ self._rafId = requestAnimationFrame(function () {
649
+ self._rafId = null;
650
+ self._windowed ? self._renderWindowed() : self._renderVirtual();
651
+ });
652
+ };
653
+
654
+ scrollTarget.addEventListener('scroll', this._scrollHandler, { passive: true });
655
+ window.addEventListener('resize', this._scrollHandler, { passive: true });
656
+ };
657
+
658
+ _component.prototype._disableVirtualScroll = function () {
659
+ if (!this._virtual) return;
660
+ this._virtual = false;
661
+
662
+ if (this._scrollHandler) {
663
+ const scrollTarget = this._scrollContainer || window;
664
+ scrollTarget.removeEventListener('scroll', this._scrollHandler);
665
+ window.removeEventListener('resize', this._scrollHandler);
666
+ this._scrollHandler = null;
667
+ }
668
+ this._scrollContainer = null;
669
+ if (this._rafId) {
670
+ cancelAnimationFrame(this._rafId);
671
+ this._rafId = null;
672
+ }
673
+ this._vStart = -1;
674
+ this._vEnd = -1;
675
+ };
676
+
677
+ _component.prototype._renderVirtual = function () {
678
+ const data = this._filteredData;
679
+ const total = data.length;
680
+ const rowH = this._rowHeight;
681
+ if (!rowH || !total) return;
682
+
683
+ const theadH = this.thead ? this.thead.offsetHeight : 0;
684
+ const sc = this._scrollContainer;
685
+ let scrollIntoData;
686
+ let viewportH;
687
+
688
+ if (sc) {
689
+ const tableRect = this.table.getBoundingClientRect();
690
+ const scRect = sc.getBoundingClientRect();
691
+ const dataStartInContainer = (tableRect.top - scRect.top) + sc.scrollTop + theadH;
692
+ scrollIntoData = sc.scrollTop - dataStartInContainer;
693
+ viewportH = sc.clientHeight;
694
+ } else {
695
+ const tableRect = this.table.getBoundingClientRect();
696
+ const tableTopInPage = tableRect.top + window.scrollY;
697
+ const dataStartInPage = tableTopInPage + theadH;
698
+ scrollIntoData = window.scrollY - dataStartInPage;
699
+ viewportH = window.innerHeight;
700
+ }
701
+
702
+ const vw = calculateVirtualWindow(scrollIntoData, viewportH, rowH, total, BUFFER_ROWS);
703
+ const startRow = vw.start;
704
+ const endRow = vw.end;
705
+
706
+ if (startRow === this._vStart && endRow === this._vEnd) return;
707
+ this._vStart = startRow;
708
+ this._vEnd = endRow;
709
+
710
+ const colSpan = this.ths.length || 1;
711
+ const topH = vw.topPadding;
712
+ const bottomH = vw.bottomPadding;
713
+
714
+ if (this.isDataDriven) {
715
+ const frag = document.createDocumentFragment();
716
+
717
+ if (topH > 0) {
718
+ const topSpacer = document.createElement('tr');
719
+ topSpacer.className = 'ln-table__spacer';
720
+ topSpacer.setAttribute('aria-hidden', 'true');
721
+ const topTd = document.createElement('td');
722
+ topTd.setAttribute('colspan', colSpan);
723
+ topTd.style.height = topH + 'px';
724
+ topSpacer.appendChild(topTd);
725
+ frag.appendChild(topSpacer);
726
+ }
727
+
728
+ for (let i = startRow; i < endRow; i++) {
729
+ const tr = this._buildRow(data[i]);
730
+ if (tr) frag.appendChild(tr);
731
+ }
732
+
733
+ if (bottomH > 0) {
734
+ const bottomSpacer = document.createElement('tr');
735
+ bottomSpacer.className = 'ln-table__spacer';
736
+ bottomSpacer.setAttribute('aria-hidden', 'true');
737
+ const bottomTd = document.createElement('td');
738
+ bottomTd.setAttribute('colspan', colSpan);
739
+ bottomTd.style.height = bottomH + 'px';
740
+ bottomSpacer.appendChild(bottomTd);
741
+ frag.appendChild(bottomSpacer);
742
+ }
743
+
744
+ this.tbody.replaceChildren(frag);
745
+
746
+ if (this._selectable) this._updateSelectAll();
747
+ } else {
748
+ let html = '';
749
+
750
+ if (topH > 0) {
751
+ html += '<tr class="ln-table__spacer" aria-hidden="true"><td colspan="' +
752
+ colSpan + '" style="height:' + topH + 'px;padding:0;border:none"></td></tr>';
753
+ }
754
+ for (let i = startRow; i < endRow; i++) html += data[i].html;
755
+ if (bottomH > 0) {
756
+ html += '<tr class="ln-table__spacer" aria-hidden="true"><td colspan="' +
757
+ colSpan + '" style="height:' + bottomH + 'px;padding:0;border:none"></td></tr>';
758
+ }
759
+
760
+ this.tbody.innerHTML = html;
761
+
762
+ if (this._selectable) this._restoreSelection();
763
+ }
764
+ };
765
+
766
+ // ─── Windowed virtualization (opt-in sliding window) ───────
767
+
768
+ _component.prototype._buildPlaceholderRow = function () {
769
+ const tr = document.createElement('tr');
770
+ tr.className = 'ln-table__placeholder';
771
+ tr.setAttribute('aria-hidden', 'true');
772
+ const td = document.createElement('td');
773
+ td.setAttribute('colspan', this.ths.length || 1);
774
+ td.style.height = this._rowHeight + 'px';
775
+ tr.appendChild(td);
776
+ return tr;
777
+ };
778
+
779
+ _component.prototype._renderWindowed = function () {
780
+ if (this.isLoaded && this._cache.logicalTotal === 0) {
781
+ this._disableVirtualScroll();
782
+ this._showEmptyState();
783
+ return;
784
+ }
785
+
786
+ if (!this._virtual) this._enableVirtualScroll();
787
+
788
+ const rowH = this._rowHeight;
789
+ if (!rowH) return;
790
+
791
+ const total = this._cache.logicalTotal;
792
+ const theadH = this.thead ? this.thead.offsetHeight : 0;
793
+ const sc = this._scrollContainer;
794
+ let scrollIntoData;
795
+ let viewportH;
796
+
797
+ if (sc) {
798
+ const tableRect = this.table.getBoundingClientRect();
799
+ const scRect = sc.getBoundingClientRect();
800
+ const dataStartInContainer = (tableRect.top - scRect.top) + sc.scrollTop + theadH;
801
+ scrollIntoData = sc.scrollTop - dataStartInContainer;
802
+ viewportH = sc.clientHeight;
803
+ } else {
804
+ const tableRect = this.table.getBoundingClientRect();
805
+ const tableTopInPage = tableRect.top + window.scrollY;
806
+ const dataStartInPage = tableTopInPage + theadH;
807
+ scrollIntoData = window.scrollY - dataStartInPage;
808
+ viewportH = window.innerHeight;
809
+ }
810
+
811
+ const vw = calculateVirtualWindow(scrollIntoData, viewportH, rowH, total, BUFFER_ROWS);
812
+ const startRow = vw.start;
813
+ const endRow = vw.end;
814
+
815
+ const colSpan = this.ths.length || 1;
816
+ const topH = vw.topPadding;
817
+ const bottomH = vw.bottomPadding;
818
+
819
+ const frag = document.createDocumentFragment();
820
+
821
+ if (topH > 0) {
822
+ const topSpacer = document.createElement('tr');
823
+ topSpacer.className = 'ln-table__spacer';
824
+ topSpacer.setAttribute('aria-hidden', 'true');
825
+ const topTd = document.createElement('td');
826
+ topTd.setAttribute('colspan', colSpan);
827
+ topTd.style.height = topH + 'px';
828
+ topSpacer.appendChild(topTd);
829
+ frag.appendChild(topSpacer);
830
+ }
831
+
832
+ for (let i = startRow; i < endRow; i++) {
833
+ if (this._cache.has(i)) {
834
+ const tr = this._buildRow(this._cache.get(i));
835
+ if (tr) frag.appendChild(tr);
836
+ } else {
837
+ frag.appendChild(this._buildPlaceholderRow());
838
+ }
839
+ }
840
+
841
+ if (bottomH > 0) {
842
+ const bottomSpacer = document.createElement('tr');
843
+ bottomSpacer.className = 'ln-table__spacer';
844
+ bottomSpacer.setAttribute('aria-hidden', 'true');
845
+ const bottomTd = document.createElement('td');
846
+ bottomTd.setAttribute('colspan', colSpan);
847
+ bottomTd.style.height = bottomH + 'px';
848
+ bottomSpacer.appendChild(bottomTd);
849
+ frag.appendChild(bottomSpacer);
850
+ }
851
+
852
+ this.tbody.replaceChildren(frag);
853
+
854
+ // Select-all is disabled in windowed mode (D4) — no _updateSelectAll() call.
855
+ this._vStart = startRow;
856
+ this._vEnd = endRow;
857
+
858
+ this._cache.ensure(startRow, endRow);
859
+ };
860
+
861
+ // ─── Empty state ───────────────────────────────────────────
862
+
863
+ _component.prototype._showEmptyState = function () {
864
+ const colSpan = this.ths.length || 1;
865
+ let emptyEl = null;
866
+
867
+ let clone = null;
868
+
869
+ if (this.isDataDriven) {
870
+ const total = this._lastTotal != null ? this._lastTotal : this._data.length;
871
+ const filtered = this.visibleCount;
872
+ // The source owns the query (docs/architecture/shared-query.md), so the view
873
+ // cannot read a search term off itself — it only knows the source narrowed
874
+ // to zero while still holding records.
875
+ const isFiltered = filtered === 0 && total > 0;
876
+
877
+ const templateName = isFiltered ? (this.name + '-empty-filtered') : (this.name + '-empty');
878
+ clone = cloneTemplateScoped(this.dom, templateName, 'ln-table');
879
+
880
+ if (!clone) {
881
+ const genericTpl = this.dom.querySelector('template[data-ln-table-empty]');
882
+ if (genericTpl) {
883
+ const whenVal = isFiltered ? 'search' : 'initial';
884
+ const subEl = genericTpl.content.querySelector('[data-ln-table-empty-when="' + whenVal + '"]')
885
+ || genericTpl.content.firstElementChild;
886
+ if (subEl) {
887
+ clone = document.importNode(subEl, true);
888
+ }
889
+ }
890
+ }
891
+
892
+ if (clone) {
893
+ if (clone.tagName === 'TR') {
894
+ emptyEl = clone;
895
+ } else {
896
+ const td = document.createElement('td');
897
+ td.setAttribute('colspan', String(colSpan));
898
+ td.appendChild(clone);
899
+ const tr = document.createElement('tr');
900
+ tr.className = 'ln-table__empty';
901
+ tr.appendChild(td);
902
+ emptyEl = tr;
903
+ }
904
+ }
905
+ } else {
906
+ const tpl = this.dom.querySelector('template[' + EMPTY_TEMPLATE + ']');
907
+ const td = document.createElement('td');
908
+ td.setAttribute('colspan', String(colSpan));
909
+ if (tpl) td.appendChild(document.importNode(tpl.content, true));
910
+
911
+ const tr = document.createElement('tr');
912
+ tr.className = 'ln-table__empty';
913
+ tr.appendChild(td);
914
+ emptyEl = tr;
915
+ }
916
+
917
+ if (emptyEl) {
918
+ this.tbody.replaceChildren(emptyEl);
919
+ } else {
920
+ this.tbody.replaceChildren();
921
+ }
922
+
923
+ dispatch(this.dom, 'ln-table:empty', {
924
+ term: this.isDataDriven ? (this.currentSearch || '') : this._searchTerm,
925
+ total: this.isDataDriven ? (this._lastTotal != null ? this._lastTotal : this._data.length) : this._data.length
926
+ });
927
+ };
928
+
929
+ // ─── Templating / Row helpers ──────────────────────────────
930
+
931
+ _component.prototype._fillRow = function (tr, record) {
932
+ fillTemplate(tr, record);
933
+
934
+ const cellAttrs = tr.querySelectorAll('[data-ln-table-cell-attr]');
935
+ for (let i = 0; i < cellAttrs.length; i++) {
936
+ const el = cellAttrs[i];
937
+ const pairs = el.getAttribute('data-ln-table-cell-attr').split(',');
938
+ for (let j = 0; j < pairs.length; j++) {
939
+ const parts = pairs[j].trim().split(':');
940
+ if (parts.length !== 2) continue;
941
+ const field = parts[0].trim();
942
+ const attr = parts[1].trim();
943
+ if (record[field] != null) {
944
+ el.setAttribute(attr, record[field]);
945
+ }
946
+ }
947
+ }
948
+ };
949
+
950
+ _component.prototype._buildRow = function (record) {
951
+ let clone = cloneTemplateScoped(this.dom, this.name + '-row', 'ln-table');
952
+ if (!clone) {
953
+ const genericTpl = this.dom.querySelector('template[data-ln-table-row]');
954
+ if (genericTpl) {
955
+ clone = document.importNode(genericTpl.content, true);
956
+ }
957
+ }
958
+
959
+ let tr = clone ? (clone.querySelector('[data-ln-table-row]') || clone.firstElementChild) : null;
960
+
961
+ if (!tr) {
962
+ if (record && record.html) {
963
+ const temp = document.createElement('tbody');
964
+ temp.innerHTML = record.html;
965
+ tr = temp.firstElementChild;
966
+ } else {
967
+ tr = document.createElement('tr');
968
+ tr.setAttribute('data-ln-table-row', '');
969
+ const ths = this.ths;
970
+
971
+ for (let j = 0; j < ths.length; j++) {
972
+ const isSelectCol = ths[j].hasAttribute('data-ln-table-col-select');
973
+ const td = document.createElement('td');
974
+
975
+ if (isSelectCol) {
976
+ const cb = document.createElement('input');
977
+ cb.type = 'checkbox';
978
+ cb.setAttribute('data-ln-table-row-select', '');
979
+ cb.setAttribute('aria-label', 'Select row');
980
+ td.appendChild(cb);
981
+ } else {
982
+ const colName = ths[j].getAttribute('data-ln-table-col');
983
+ if (colName && record[colName] != null) {
984
+ td.textContent = String(record[colName]);
985
+ }
986
+ }
987
+ tr.appendChild(td);
988
+ }
989
+ }
990
+ } else {
991
+ this._fillRow(tr, record);
992
+ }
993
+
994
+ tr._lnRecord = record;
995
+ if (record.id != null) {
996
+ tr.setAttribute('data-ln-table-row-id', record.id);
997
+ }
998
+
999
+ if (this._selectable && record.id != null && this.selectedIds.has(String(record.id))) {
1000
+ tr.classList.add('ln-row-selected');
1001
+ const rowCb = tr.querySelector('[data-ln-table-row-select]');
1002
+ if (rowCb) rowCb.checked = true;
1003
+ }
1004
+
1005
+ return tr;
1006
+ };
1007
+
1008
+ _component.prototype._requestData = function () {
1009
+ if (this._windowed) {
1010
+ this.dom.classList.add('ln-table--loading');
1011
+ this._cache.invalidate({
1012
+ sort: this.currentSort,
1013
+ filters: this.currentFilters,
1014
+ search: this.currentSearch
1015
+ });
1016
+ return;
1017
+ }
1018
+ requestData(this, 'ln-table:request-data', 'table');
1019
+ };
1020
+
1021
+ // ─── Windowed Mode — enter/exit/seed (live toggle) ──────────
1022
+
1023
+ _component.prototype._enterWindowedMode = function () {
1024
+ const self = this;
1025
+ const dom = this.dom;
1026
+ const winAttr = parseInt(dom.getAttribute('data-ln-table-window'), 10);
1027
+ const pageAttr = parseInt(dom.getAttribute('data-ln-table-window-page'), 10);
1028
+ const threshAttr = parseInt(dom.getAttribute('data-ln-table-window-threshold'), 10);
1029
+
1030
+ // Cache change → sync totals off the cache, re-render (pull), notify.
1031
+ this._onCacheChange = function () {
1032
+ if (!self._windowed || !self._cache) return;
1033
+ self.totalCount = self._cache.grandTotal;
1034
+ self.visibleCount = self._cache.logicalTotal;
1035
+ self._lastTotal = self._cache.grandTotal;
1036
+ self.isLoaded = true;
1037
+ self._vStart = -1;
1038
+ self._vEnd = -1;
1039
+ self._render();
1040
+ self._updateFooter();
1041
+ dispatch(dom, 'ln-table:rendered', {
1042
+ table: self.name,
1043
+ total: self.totalCount,
1044
+ visible: self.visibleCount
1045
+ });
1046
+ };
1047
+ this._renderBatch = createBatcher(this._onCacheChange);
1048
+
1049
+ this._cache = createWindowCache({
1050
+ windowSize: winAttr > 0 ? winAttr : WINDOW_DEFAULT,
1051
+ pageSize: pageAttr > 0 ? pageAttr : WINDOW_PAGE,
1052
+ threshold: threshAttr >= 0 ? threshAttr : 25,
1053
+ fetchDebounce: FETCH_DEBOUNCE,
1054
+ requestPage: function (query, offset, limit) {
1055
+ dispatch(dom, 'ln-table:request-data', {
1056
+ table: self.name,
1057
+ sort: query.sort,
1058
+ filters: query.filters,
1059
+ search: query.search,
1060
+ offset: offset,
1061
+ limit: limit,
1062
+ queryGen: self._cache.queryGen
1063
+ });
1064
+ },
1065
+ onChange: this._renderBatch
1066
+ });
1067
+
1068
+ this._windowed = true;
1069
+
1070
+ // D4 — windowed tables cannot select rows they have never fetched
1071
+ if (this._selectable && this._selectAllCheckbox) {
1072
+ this._selectAllCheckbox.classList.add('hidden');
1073
+ }
1074
+ };
1075
+
1076
+ _component.prototype._kickWindowInitial = function () {
1077
+ if (this._data.length > 0) {
1078
+ // SSR-seeded / warm-seeded: page 0 is already resident, the grand
1079
+ // total is declared in markup — no initial fetch needed. No
1080
+ // queryGen on the seed — it must never be dropped as stale.
1081
+ let declaredTotal = parseInt(this.dom.getAttribute('data-ln-table-count'), 10);
1082
+ if (isNaN(declaredTotal) && this._totalSpan) {
1083
+ const spanText = this._totalSpan.textContent.replace(/[^\d]/g, '');
1084
+ if (spanText) declaredTotal = parseInt(spanText, 10);
1085
+ }
1086
+ const seedTotal = (declaredTotal > 0) ? declaredTotal : this._data.length;
1087
+ this._cache.ingest({
1088
+ data: this._data,
1089
+ offset: 0,
1090
+ total: seedTotal,
1091
+ filtered: seedTotal
1092
+ });
1093
+ } else {
1094
+ this.dom.classList.add('ln-table--loading');
1095
+ this._cache.requestInitial({
1096
+ sort: this.currentSort,
1097
+ filters: this.currentFilters,
1098
+ search: this.currentSearch
1099
+ });
1100
+ }
1101
+ };
1102
+
1103
+ _component.prototype._exitWindowedMode = function () {
1104
+ this._disableVirtualScroll();
1105
+ if (this._cache) this._cache.destroy();
1106
+ this._cache = null;
1107
+ this._windowed = false;
1108
+ this._renderBatch = null;
1109
+ this._onCacheChange = null;
1110
+ if (this._selectAllCheckbox) {
1111
+ this._selectAllCheckbox.classList.remove('hidden');
1112
+ }
1113
+ this._rowHeight = 0;
1114
+ this._vStart = -1;
1115
+ this._vEnd = -1;
1116
+ this._data = [];
1117
+ this._filteredData = [];
1118
+ this.dom.classList.add('ln-table--loading');
1119
+ this._requestData();
1120
+ };
1121
+
1122
+ // ─── Selection Helpers ─────────────────────────────────────
1123
+
1124
+ _component.prototype._updateSelectAll = function () {
1125
+ if (!this._selectAllCheckbox || !this.tbody) return;
1126
+ const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1127
+ const ids = [];
1128
+ for (let i = 0; i < rows.length; i++) {
1129
+ const id = rows[i].getAttribute('data-ln-table-row-id');
1130
+ if (id != null) ids.push(id);
1131
+ }
1132
+ const state = calculateSelectionState(ids, this.selectedIds);
1133
+ this._selectAllCheckbox.checked = state.isAllSelected;
1134
+ this._selectAllCheckbox.indeterminate = state.isIndeterminate;
1135
+ };
1136
+
1137
+ // SSR re-render replays cached row HTML — reapply live selection state.
1138
+ _component.prototype._restoreSelection = function () {
1139
+ if (!this.tbody) return;
1140
+ const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1141
+ for (let i = 0; i < rows.length; i++) {
1142
+ const id = rows[i].getAttribute('data-ln-table-row-id');
1143
+ const selected = id != null && this.selectedIds.has(id);
1144
+ rows[i].classList.toggle('ln-row-selected', selected);
1145
+ const cb = rows[i].querySelector('[data-ln-table-row-select]');
1146
+ if (cb) cb.checked = selected;
1147
+ }
1148
+ this._updateSelectAll();
1149
+ };
1150
+
1151
+ Object.defineProperty(_component.prototype, 'selectedCount', {
1152
+ get: function () { return this.selectedIds.size; },
1153
+ set: function () { /* computed from selectedIds */ }
1154
+ });
1155
+
1156
+ _component.prototype._enableSelection = function () {
1157
+ if (this._selectableActive) return;
1158
+ this._selectableActive = true;
1159
+
1160
+ const self = this;
1161
+ this._onSelectionChange = function (e) {
1162
+ const checkbox = e.target.closest('[data-ln-table-row-select]');
1163
+ if (!checkbox) return;
1164
+ const tr = checkbox.closest('[data-ln-table-row]');
1165
+ if (!tr) return;
1166
+ const id = tr.getAttribute('data-ln-table-row-id');
1167
+ if (id == null) return;
1168
+
1169
+ self.selectedIds = toggleRowSelection(self.selectedIds, id, checkbox.checked);
1170
+ tr.classList.toggle('ln-row-selected', checkbox.checked);
1171
+
1172
+ self.selectedCount = self.selectedIds.size;
1173
+ self._updateSelectAll();
1174
+ self._updateFooter();
1175
+
1176
+ dispatch(self.dom, 'ln-table:select', {
1177
+ table: self.name,
1178
+ selectedIds: self.selectedIds,
1179
+ count: self.selectedCount
1180
+ });
1181
+ };
1182
+ if (this.tbody) this.tbody.addEventListener('change', this._onSelectionChange);
1183
+
1184
+ this._selectAllCheckbox = this.dom.querySelector('[data-ln-table-col-select] input[type="checkbox"]')
1185
+ || this.dom.querySelector('[data-ln-table-col-select]');
1186
+ if (this._selectAllCheckbox && this._selectAllCheckbox.tagName === 'TH') {
1187
+ const cb = document.createElement('input');
1188
+ cb.type = 'checkbox';
1189
+ const dictEl = self.dom.querySelector('[data-ln-table-dict="select-all"]');
1190
+ const label = self.dom.getAttribute('data-ln-table-select-all-label') || (dictEl ? dictEl.textContent.trim() : null) || 'Select all';
1191
+ cb.setAttribute('aria-label', label);
1192
+ this._selectAllCheckbox.appendChild(cb);
1193
+ this._selectAllCheckbox = cb;
1194
+ }
1195
+
1196
+ if (this._selectAllCheckbox) {
1197
+ this._onSelectAll = function () {
1198
+ const checked = self._selectAllCheckbox.checked;
1199
+ const rows = self.tbody ? self.tbody.querySelectorAll('[data-ln-table-row]') : [];
1200
+ const ids = [];
1201
+
1202
+ for (let i = 0; i < rows.length; i++) {
1203
+ const id = rows[i].getAttribute('data-ln-table-row-id');
1204
+ const rowCb = rows[i].querySelector('[data-ln-table-row-select]');
1205
+ if (id == null) continue;
1206
+ ids.push(id);
1207
+ rows[i].classList.toggle('ln-row-selected', checked);
1208
+ if (rowCb) rowCb.checked = checked;
1209
+ }
1210
+
1211
+ self.selectedIds = toggleSelectAll(self.selectedIds, ids, checked);
1212
+ self.selectedCount = self.selectedIds.size;
1213
+ dispatch(self.dom, 'ln-table:select-all', {
1214
+ table: self.name,
1215
+ selected: checked
1216
+ });
1217
+ dispatch(self.dom, 'ln-table:select', {
1218
+ table: self.name,
1219
+ selectedIds: self.selectedIds,
1220
+ count: self.selectedCount
1221
+ });
1222
+ self._updateFooter();
1223
+ };
1224
+ this._selectAllCheckbox.addEventListener('change', this._onSelectAll);
1225
+ }
1226
+
1227
+ if (this.tbody) {
1228
+ const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1229
+ for (let i = 0; i < rows.length; i++) {
1230
+ const cb = rows[i].querySelector('[data-ln-table-row-select]');
1231
+ const id = rows[i].getAttribute('data-ln-table-row-id');
1232
+ if (cb && cb.checked && id != null) {
1233
+ self.selectedIds = toggleRowSelection(self.selectedIds, id, true);
1234
+ rows[i].classList.add('ln-row-selected');
1235
+ }
1236
+ }
1237
+ this.selectedCount = this.selectedIds.size;
1238
+ if (this.selectedCount > 0) this._updateSelectAll();
1239
+ }
1240
+ };
1241
+
1242
+ _component.prototype._disableSelection = function () {
1243
+ if (!this._selectableActive) return;
1244
+ this._selectableActive = false;
1245
+
1246
+ if (this.tbody && this._onSelectionChange) {
1247
+ this.tbody.removeEventListener('change', this._onSelectionChange);
1248
+ }
1249
+ if (this._selectAllCheckbox && this._onSelectAll) {
1250
+ this._selectAllCheckbox.removeEventListener('change', this._onSelectAll);
1251
+ }
1252
+
1253
+ const th = this.dom.querySelector('[data-ln-table-col-select]');
1254
+ if (th) {
1255
+ const cb = th.querySelector('input[type="checkbox"]');
1256
+ if (cb) {
1257
+ cb.remove();
1258
+ }
1259
+ }
1260
+ this._selectAllCheckbox = null;
1261
+
1262
+ this.selectedIds = toggleSelectAll(this.selectedIds, Array.from(this.selectedIds), false);
1263
+ this.selectedCount = 0;
1264
+
1265
+ if (this.tbody) {
1266
+ const rows = this.tbody.querySelectorAll('[data-ln-table-row]');
1267
+ for (let i = 0; i < rows.length; i++) {
1268
+ rows[i].classList.remove('ln-row-selected');
1269
+ const cb = rows[i].querySelector('[data-ln-table-row-select]');
1270
+ if (cb) cb.checked = false;
1271
+ }
1272
+ }
1273
+
1274
+ this._updateFooter();
1275
+ };
1276
+
1277
+ // ─── Footer Helpers ────────────────────────────────────────
1278
+
1279
+ _component.prototype._updateFooter = function () {
1280
+ let total = 0;
1281
+ let filtered = 0;
1282
+
1283
+ if (this.isDataDriven) {
1284
+ total = this._lastTotal != null ? this._lastTotal : this._data.length;
1285
+ filtered = this.visibleCount;
1286
+ } else {
1287
+ total = this._data.length;
1288
+ filtered = this._filteredData.length;
1289
+ }
1290
+
1291
+ const isFiltered = filtered < total;
1292
+
1293
+ if (this._totalSpan) {
1294
+ this._totalSpan.textContent = _formatNum(total, this.dom);
1295
+ }
1296
+
1297
+ if (this._filteredSpan) {
1298
+ this._filteredSpan.textContent = isFiltered ? _formatNum(filtered, this.dom) : '';
1299
+ }
1300
+
1301
+ if (this._filteredWrap) {
1302
+ this._filteredWrap.classList.toggle('hidden', !isFiltered);
1303
+ }
1304
+
1305
+ if (this._selectedSpan) {
1306
+ const count = this.selectedIds ? this.selectedIds.size : 0;
1307
+ this._selectedSpan.textContent = count > 0 ? _formatNum(count, this.dom) : '';
1308
+ if (this._selectedWrap) {
1309
+ this._selectedWrap.classList.toggle('hidden', count === 0);
1310
+ }
1311
+ }
1312
+ };
1313
+
1314
+
1315
+
1316
+ // ─── Destroy ───────────────────────────────────────────────
1317
+
1318
+ _component.prototype.destroy = function () {
1319
+ if (!this.dom[DOM_ATTRIBUTE]) return;
1320
+ this._disableVirtualScroll();
1321
+
1322
+ this.dom.removeEventListener('ln-table:set-search', this._onSetSearch);
1323
+ this.dom.removeEventListener('ln-table:request-clear-filters', this._onRequestClearFilters);
1324
+
1325
+ if (this.isDataDriven) {
1326
+ this.dom.removeEventListener('ln-table:set-data', this._onSetData);
1327
+ this.dom.removeEventListener('ln-table:set-loading', this._onSetLoading);
1328
+ this.dom.removeEventListener('ln-table:page-failed', this._onPageFailed);
1329
+ this.dom.removeEventListener('ln-table:request-revalidate', this._onRequestRevalidate);
1330
+ this.dom.removeEventListener('ln-table:request-invalidate', this._onRequestInvalidate);
1331
+ this.dom.removeEventListener('ln-sort:change', this._onSort);
1332
+ if (this.tbody) {
1333
+ this.tbody.removeEventListener('click', this._onRowClick);
1334
+ this.tbody.removeEventListener('click', this._onRowAction);
1335
+ }
1336
+ if (this._cache) this._cache.destroy();
1337
+ } else {
1338
+ if (this._emptyTbodyObserver) {
1339
+ this._emptyTbodyObserver.disconnect();
1340
+ this._emptyTbodyObserver = null;
1341
+ }
1342
+ this.dom.removeEventListener('ln-sort:change', this._onSort);
1343
+ this.dom.removeEventListener('ln-search:change', this._onSearchChange);
1344
+ this.dom.removeEventListener('ln-filter:change', this._onFilterChange);
1345
+ }
1346
+
1347
+ if (this._onSelectionChange && this.tbody) this.tbody.removeEventListener('change', this._onSelectionChange);
1348
+ if (this._selectAllCheckbox && this._onSelectAll) this._selectAllCheckbox.removeEventListener('change', this._onSelectAll);
1349
+
1350
+ if (this._colgroup) {
1351
+ this._colgroup.remove();
1352
+ this._colgroup = null;
1353
+ }
1354
+ if (this.table) this.table.style.tableLayout = '';
1355
+ this._data = [];
1356
+ this._filteredData = [];
1357
+ delete this.dom[DOM_ATTRIBUTE];
1358
+ };
1359
+
1360
+ // ─── Init ──────────────────────────────────────────────────
1361
+
1362
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-table', {
1363
+ attributes: ATTRIBUTES
1364
+ });
1365
+ })();