@livenetworks/ashlar 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +177 -0
  2. package/js/COMPONENTS.md +1102 -0
  3. package/js/index.js +41 -0
  4. package/js/ln-accordion/README.md +137 -0
  5. package/js/ln-accordion/ln-accordion.js +1 -0
  6. package/js/ln-accordion/src/ln-accordion.js +41 -0
  7. package/js/ln-ajax/README.md +91 -0
  8. package/js/ln-ajax/ln-ajax.js +1 -0
  9. package/js/ln-ajax/src/ln-ajax.js +277 -0
  10. package/js/ln-api-connector/README.md +150 -0
  11. package/js/ln-api-connector/ln-api-connector.js +1 -0
  12. package/js/ln-api-connector/src/ln-api-connector.js +265 -0
  13. package/js/ln-autoresize/README.md +80 -0
  14. package/js/ln-autoresize/ln-autoresize.js +1 -0
  15. package/js/ln-autoresize/src/ln-autoresize.js +47 -0
  16. package/js/ln-autosave/README.md +92 -0
  17. package/js/ln-autosave/ln-autosave.js +1 -0
  18. package/js/ln-autosave/src/ln-autosave.js +147 -0
  19. package/js/ln-circular-progress/README.md +161 -0
  20. package/js/ln-circular-progress/ln-circular-progress.js +1 -0
  21. package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
  22. package/js/ln-confirm/README.md +86 -0
  23. package/js/ln-confirm/_ln-confirm.scss +13 -0
  24. package/js/ln-confirm/ln-confirm.js +1 -0
  25. package/js/ln-confirm/src/ln-confirm.js +131 -0
  26. package/js/ln-core/crypto.js +83 -0
  27. package/js/ln-core/helpers.js +411 -0
  28. package/js/ln-core/index.js +5 -0
  29. package/js/ln-core/persist.js +71 -0
  30. package/js/ln-core/positioning.js +207 -0
  31. package/js/ln-core/reactive.js +74 -0
  32. package/js/ln-couchdb-connector/README.md +156 -0
  33. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
  34. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
  35. package/js/ln-data-coordinator/README.md +165 -0
  36. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
  37. package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
  38. package/js/ln-data-store/README.md +94 -0
  39. package/js/ln-data-store/ln-data-store.js +1 -0
  40. package/js/ln-data-store/src/ln-data-store.js +699 -0
  41. package/js/ln-data-table/README.md +110 -0
  42. package/js/ln-data-table/ln-data-table.js +1 -0
  43. package/js/ln-data-table/ln-data-table.scss +10 -0
  44. package/js/ln-data-table/src/ln-data-table.js +1103 -0
  45. package/js/ln-date/README.md +151 -0
  46. package/js/ln-date/ln-date.js +1 -0
  47. package/js/ln-date/src/ln-date.js +442 -0
  48. package/js/ln-dropdown/README.md +117 -0
  49. package/js/ln-dropdown/ln-dropdown.js +1 -0
  50. package/js/ln-dropdown/ln-dropdown.scss +15 -0
  51. package/js/ln-dropdown/src/ln-dropdown.js +174 -0
  52. package/js/ln-external-links/README.md +341 -0
  53. package/js/ln-external-links/ln-external-links.js +1 -0
  54. package/js/ln-external-links/src/ln-external-links.js +116 -0
  55. package/js/ln-filter/README.md +99 -0
  56. package/js/ln-filter/ln-filter.js +1 -0
  57. package/js/ln-filter/ln-filter.scss +7 -0
  58. package/js/ln-filter/src/ln-filter.js +404 -0
  59. package/js/ln-form/README.md +101 -0
  60. package/js/ln-form/ln-form.js +1 -0
  61. package/js/ln-form/src/ln-form.js +199 -0
  62. package/js/ln-http/README.md +89 -0
  63. package/js/ln-http/ln-http.js +1 -0
  64. package/js/ln-http/src/ln-http.js +219 -0
  65. package/js/ln-icons/README.md +88 -0
  66. package/js/ln-icons/ln-icons.js +1 -0
  67. package/js/ln-icons/src/ln-icons.js +169 -0
  68. package/js/ln-link/README.md +303 -0
  69. package/js/ln-link/ln-link.js +1 -0
  70. package/js/ln-link/src/ln-link.js +196 -0
  71. package/js/ln-modal/README.md +154 -0
  72. package/js/ln-modal/ln-modal.js +1 -0
  73. package/js/ln-modal/ln-modal.scss +11 -0
  74. package/js/ln-modal/src/ln-modal.js +201 -0
  75. package/js/ln-nav/README.md +70 -0
  76. package/js/ln-nav/ln-nav.js +1 -0
  77. package/js/ln-nav/src/ln-nav.js +177 -0
  78. package/js/ln-number/README.md +122 -0
  79. package/js/ln-number/ln-number.js +1 -0
  80. package/js/ln-number/src/ln-number.js +302 -0
  81. package/js/ln-popover/README.md +127 -0
  82. package/js/ln-popover/ln-popover.js +1 -0
  83. package/js/ln-popover/src/ln-popover.js +288 -0
  84. package/js/ln-progress/README.md +442 -0
  85. package/js/ln-progress/ln-progress.js +1 -0
  86. package/js/ln-progress/src/ln-progress.js +150 -0
  87. package/js/ln-search/README.md +83 -0
  88. package/js/ln-search/ln-search.js +1 -0
  89. package/js/ln-search/ln-search.scss +7 -0
  90. package/js/ln-search/src/ln-search.js +114 -0
  91. package/js/ln-sortable/README.md +95 -0
  92. package/js/ln-sortable/ln-sortable.js +1 -0
  93. package/js/ln-sortable/src/ln-sortable.js +203 -0
  94. package/js/ln-table/README.md +101 -0
  95. package/js/ln-table/ln-table-sort.js +1 -0
  96. package/js/ln-table/ln-table.js +1 -0
  97. package/js/ln-table/ln-table.scss +11 -0
  98. package/js/ln-table/src/ln-table-sort.js +168 -0
  99. package/js/ln-table/src/ln-table.js +473 -0
  100. package/js/ln-tabs/README.md +137 -0
  101. package/js/ln-tabs/ln-tabs.js +1 -0
  102. package/js/ln-tabs/src/ln-tabs.js +171 -0
  103. package/js/ln-time/README.md +81 -0
  104. package/js/ln-time/ln-time.js +1 -0
  105. package/js/ln-time/src/ln-time.js +192 -0
  106. package/js/ln-toast/README.md +122 -0
  107. package/js/ln-toast/ln-toast.js +15 -0
  108. package/js/ln-toast/src/ln-toast.js +210 -0
  109. package/js/ln-toast/template.html +14 -0
  110. package/js/ln-toggle/README.md +137 -0
  111. package/js/ln-toggle/ln-toggle.js +1 -0
  112. package/js/ln-toggle/src/ln-toggle.js +139 -0
  113. package/js/ln-tooltip/README.md +58 -0
  114. package/js/ln-tooltip/ln-tooltip.js +1 -0
  115. package/js/ln-tooltip/ln-tooltip.scss +9 -0
  116. package/js/ln-tooltip/src/ln-tooltip.js +169 -0
  117. package/js/ln-translations/README.md +96 -0
  118. package/js/ln-translations/ln-translations.js +1 -0
  119. package/js/ln-translations/src/ln-translations.js +275 -0
  120. package/js/ln-upload/README.md +180 -0
  121. package/js/ln-upload/ln-upload.js +1 -0
  122. package/js/ln-upload/ln-upload.scss +20 -0
  123. package/js/ln-upload/src/ln-upload.js +407 -0
  124. package/js/ln-validate/README.md +108 -0
  125. package/js/ln-validate/ln-validate.js +1 -0
  126. package/js/ln-validate/src/ln-validate.js +160 -0
  127. package/package.json +55 -0
  128. package/scss/base/_global.scss +83 -0
  129. package/scss/base/_reset.scss +17 -0
  130. package/scss/base/_typography.scss +125 -0
  131. package/scss/components/_accordion.scss +34 -0
  132. package/scss/components/_ajax.scss +15 -0
  133. package/scss/components/_alert.scss +5 -0
  134. package/scss/components/_app-shell.scss +15 -0
  135. package/scss/components/_avatar.scss +6 -0
  136. package/scss/components/_breadcrumbs.scss +33 -0
  137. package/scss/components/_button.scss +20 -0
  138. package/scss/components/_card.scss +10 -0
  139. package/scss/components/_chip.scss +5 -0
  140. package/scss/components/_circular-progress.scss +29 -0
  141. package/scss/components/_confirm.scss +5 -0
  142. package/scss/components/_data-table.scss +83 -0
  143. package/scss/components/_dropdown.scss +25 -0
  144. package/scss/components/_empty-state.scss +22 -0
  145. package/scss/components/_form.scss +100 -0
  146. package/scss/components/_layout.scss +8 -0
  147. package/scss/components/_link.scss +11 -0
  148. package/scss/components/_ln-table.scss +60 -0
  149. package/scss/components/_loader.scss +6 -0
  150. package/scss/components/_modal.scss +20 -0
  151. package/scss/components/_nav.scss +9 -0
  152. package/scss/components/_page-header.scss +10 -0
  153. package/scss/components/_popover.scss +10 -0
  154. package/scss/components/_progress.scss +17 -0
  155. package/scss/components/_prose.scss +5 -0
  156. package/scss/components/_scrollbar.scss +32 -0
  157. package/scss/components/_sections.scss +12 -0
  158. package/scss/components/_sidebar.scss +5 -0
  159. package/scss/components/_stat-card.scss +5 -0
  160. package/scss/components/_status-badge.scss +4 -0
  161. package/scss/components/_stepper.scss +5 -0
  162. package/scss/components/_table.scss +19 -0
  163. package/scss/components/_tabs.scss +21 -0
  164. package/scss/components/_timeline.scss +14 -0
  165. package/scss/components/_toast.scss +41 -0
  166. package/scss/components/_toggle.scss +81 -0
  167. package/scss/components/_tooltip.scss +18 -0
  168. package/scss/components/_translations.scss +111 -0
  169. package/scss/components/_upload.scss +51 -0
  170. package/scss/config/_breakpoints.scss +72 -0
  171. package/scss/config/_density.scss +117 -0
  172. package/scss/config/_icons.scss +37 -0
  173. package/scss/config/_mixins.scss +13 -0
  174. package/scss/config/_theme.scss +216 -0
  175. package/scss/config/_tokens.scss +419 -0
  176. package/scss/config/mixins/_accordion.scss +52 -0
  177. package/scss/config/mixins/_ajax.scss +39 -0
  178. package/scss/config/mixins/_alert.scss +82 -0
  179. package/scss/config/mixins/_app-shell.scss +312 -0
  180. package/scss/config/mixins/_avatar.scss +109 -0
  181. package/scss/config/mixins/_borders.scss +36 -0
  182. package/scss/config/mixins/_breadcrumbs.scss +72 -0
  183. package/scss/config/mixins/_breakpoints.scss +62 -0
  184. package/scss/config/mixins/_btn.scss +179 -0
  185. package/scss/config/mixins/_card.scss +338 -0
  186. package/scss/config/mixins/_chip.scss +66 -0
  187. package/scss/config/mixins/_circular-progress.scss +71 -0
  188. package/scss/config/mixins/_collapsible.scss +24 -0
  189. package/scss/config/mixins/_colors.scss +46 -0
  190. package/scss/config/mixins/_confirm.scss +31 -0
  191. package/scss/config/mixins/_data-table.scss +346 -0
  192. package/scss/config/mixins/_display.scss +32 -0
  193. package/scss/config/mixins/_dropdown.scss +143 -0
  194. package/scss/config/mixins/_empty-state.scss +30 -0
  195. package/scss/config/mixins/_focus.scss +55 -0
  196. package/scss/config/mixins/_footer.scss +42 -0
  197. package/scss/config/mixins/_form.scss +601 -0
  198. package/scss/config/mixins/_index.scss +58 -0
  199. package/scss/config/mixins/_interaction.scss +15 -0
  200. package/scss/config/mixins/_kbd.scss +22 -0
  201. package/scss/config/mixins/_layout.scss +117 -0
  202. package/scss/config/mixins/_link.scss +55 -0
  203. package/scss/config/mixins/_ln-table.scss +420 -0
  204. package/scss/config/mixins/_loader.scss +26 -0
  205. package/scss/config/mixins/_modal.scss +66 -0
  206. package/scss/config/mixins/_motion.scss +19 -0
  207. package/scss/config/mixins/_nav.scss +273 -0
  208. package/scss/config/mixins/_page-header.scss +69 -0
  209. package/scss/config/mixins/_popover.scss +25 -0
  210. package/scss/config/mixins/_position.scss +32 -0
  211. package/scss/config/mixins/_progress.scss +56 -0
  212. package/scss/config/mixins/_prose.scss +127 -0
  213. package/scss/config/mixins/_shadows.scss +8 -0
  214. package/scss/config/mixins/_sidebar.scss +95 -0
  215. package/scss/config/mixins/_sizing.scss +6 -0
  216. package/scss/config/mixins/_spacing.scss +19 -0
  217. package/scss/config/mixins/_stat-card.scss +68 -0
  218. package/scss/config/mixins/_status-badge.scss +83 -0
  219. package/scss/config/mixins/_stepper.scss +78 -0
  220. package/scss/config/mixins/_table.scss +215 -0
  221. package/scss/config/mixins/_tabs.scss +64 -0
  222. package/scss/config/mixins/_timeline.scss +69 -0
  223. package/scss/config/mixins/_toast.scss +148 -0
  224. package/scss/config/mixins/_tooltip.scss +111 -0
  225. package/scss/config/mixins/_transitions.scss +10 -0
  226. package/scss/config/mixins/_translations.scss +124 -0
  227. package/scss/config/mixins/_typography.scss +57 -0
  228. package/scss/config/mixins/_upload.scss +168 -0
  229. package/scss/ln-ashlar.scss +62 -0
  230. package/scss/tabler-icons.txt +5039 -0
  231. package/scss/utilities/_animations.scss +83 -0
  232. package/scss/utilities/_utilities.scss +49 -0
@@ -0,0 +1 @@
1
+ (function(){"use strict";function d(o,n,a){o.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:a||{}}))}function s(o,n){if(!document.body){document.addEventListener("DOMContentLoaded",function(){s(o,n)}),console.warn("["+n+'] Script loaded before <body> — add "defer" to your <script> tag');return}o()}const u={};function l(o,n){u[o]=n}function f(o){return u[o]||{ingress:n=>n,egress:n=>n}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=l,window.lnCore.getDataMapper=f),(function(){const o="lnExternalLinks";if(window[o]!==void 0)return;function n(e){return e.hostname&&e.hostname!==window.location.hostname}function a(e){if(e.getAttribute("data-ln-external-link")==="processed"||!n(e))return;e.target="_blank";const t=(e.rel||"").split(/\s+/).filter(Boolean);t.includes("noopener")||t.push("noopener"),t.includes("noreferrer")||t.push("noreferrer"),e.rel=t.join(" ");const i=document.createElement("span");i.className="sr-only",i.textContent="(opens in new tab)",e.appendChild(i),e.setAttribute("data-ln-external-link","processed"),d(e,"ln-external-links:processed",{link:e,href:e.href})}function c(e){e=e||document.body;for(const t of e.querySelectorAll("a, area"))a(t)}function p(){s(function(){document.body.addEventListener("click",function(e){const t=e.target.closest("a, area");t&&t.getAttribute("data-ln-external-link")==="processed"&&d(t,"ln-external-links:clicked",{link:t,href:t.href,text:t.textContent||t.title||""})})},"ln-external-links")}function b(){s(function(){new MutationObserver(function(t){for(const i of t){if(i.type==="childList"){for(const r of i.addedNodes)if(r.nodeType===1&&(r.matches&&(r.matches("a")||r.matches("area"))&&a(r),r.querySelectorAll))for(const m of r.querySelectorAll("a, area"))a(m)}if(i.type==="attributes"&&i.attributeName==="href"){const r=i.target;r.matches&&(r.matches("a")||r.matches("area"))&&a(r)}}}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["href"]})},"ln-external-links")}function h(){p(),b(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){c()}):c()}window[o]={process:c},h()})()})();
@@ -0,0 +1,116 @@
1
+ import { dispatch, guardBody } from '../../ln-core';
2
+
3
+ (function() {
4
+ const DOM_ATTRIBUTE = 'lnExternalLinks';
5
+
6
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
7
+
8
+ function _isExternalLink(link) {
9
+ return link.hostname && link.hostname !== window.location.hostname;
10
+ }
11
+
12
+ function _processLink(link) {
13
+ if (link.getAttribute('data-ln-external-link') === 'processed') return;
14
+ if (!_isExternalLink(link)) return;
15
+
16
+ link.target = '_blank';
17
+ const existing = (link.rel || '').split(/\s+/).filter(Boolean);
18
+ if (!existing.includes('noopener')) existing.push('noopener');
19
+ if (!existing.includes('noreferrer')) existing.push('noreferrer');
20
+ link.rel = existing.join(' ');
21
+
22
+ const hint = document.createElement('span');
23
+ hint.className = 'sr-only';
24
+ hint.textContent = '(opens in new tab)';
25
+ link.appendChild(hint);
26
+
27
+ link.setAttribute('data-ln-external-link', 'processed');
28
+
29
+ dispatch(link, 'ln-external-links:processed', {
30
+ link: link,
31
+ href: link.href
32
+ });
33
+ }
34
+
35
+ function _processLinks(container) {
36
+ container = container || document.body;
37
+
38
+ for (const link of container.querySelectorAll('a, area')) {
39
+ _processLink(link);
40
+ }
41
+ }
42
+
43
+ function _setupClickTracking() {
44
+ guardBody(function() {
45
+ document.body.addEventListener('click', function(e) {
46
+ const link = e.target.closest('a, area');
47
+ if (!link) return;
48
+
49
+ if (link.getAttribute('data-ln-external-link') === 'processed') {
50
+ dispatch(link, 'ln-external-links:clicked', {
51
+ link: link,
52
+ href: link.href,
53
+ text: link.textContent || link.title || ''
54
+ });
55
+ }
56
+ });
57
+ }, 'ln-external-links');
58
+ }
59
+
60
+ function _domObserver() {
61
+ guardBody(function() {
62
+ const observer = new MutationObserver(function(mutations) {
63
+ for (const mutation of mutations) {
64
+ if (mutation.type === 'childList') {
65
+ for (const node of mutation.addedNodes) {
66
+ if (node.nodeType === 1) {
67
+ if (node.matches && (node.matches('a') || node.matches('area'))) {
68
+ _processLink(node);
69
+ }
70
+
71
+ if (node.querySelectorAll) {
72
+ for (const link of node.querySelectorAll('a, area')) {
73
+ _processLink(link);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ if (mutation.type === 'attributes' && mutation.attributeName === 'href') {
81
+ const target = mutation.target;
82
+ if (target.matches && (target.matches('a') || target.matches('area'))) {
83
+ _processLink(target);
84
+ }
85
+ }
86
+ }
87
+ });
88
+
89
+ observer.observe(document.body, {
90
+ childList: true,
91
+ subtree: true,
92
+ attributes: true,
93
+ attributeFilter: ['href']
94
+ });
95
+ }, 'ln-external-links');
96
+ }
97
+
98
+ function _initialize() {
99
+ _setupClickTracking();
100
+ _domObserver();
101
+
102
+ if (document.readyState === 'loading') {
103
+ document.addEventListener('DOMContentLoaded', function() {
104
+ _processLinks();
105
+ });
106
+ } else {
107
+ _processLinks();
108
+ }
109
+ }
110
+
111
+ window[DOM_ATTRIBUTE] = {
112
+ process: _processLinks
113
+ };
114
+
115
+ _initialize();
116
+ })();
@@ -0,0 +1,99 @@
1
+ # ln-filter
2
+
3
+ A zero-dependency, event-driven **Generic List & Table Filter Primitive** that manages item visibility states through declarative checkbox controls.
4
+
5
+ It filters target elements either by comparing child dataset attributes (for custom cards/lists) or scanning table column cell contents (for plain HTML tables). It operates independently of and in harmony with `ln-search`, combining multiple filter criteria seamlessly.
6
+
7
+ ---
8
+
9
+ ## 🧭 Philosophy & Architecture
10
+
11
+ 1. **Declarative State Model:** The component has no custom imperative state-change methods. Filter state is driven entirely by native checkboxes. External scripts update selections by writing `input.checked = true` and dispatching a standard bubbled `change` event.
12
+ 2. **Sentinel Mutual Exclusion:** The `data-ln-filter-reset` ("All") checkbox is kept in sync automatically: checking any value checkbox unchecks the reset sentinel; checking the sentinel unchecks all value inputs; unchecking all value inputs re-checks the sentinel.
13
+ 3. **Table Column & Auto-Population Mode:** By defining `data-ln-filter-col="N"`, the component filters plain HTML `<table>` rows by column cell text. When a `<template>` tag is nested inside, the component automatically populates value checkboxes from the column's unique values on page load.
14
+ 4. **Local State Persistence:** Adding the `data-ln-persist` attribute saves active filter selections to `localStorage` under `lnf:{id}`, ensuring filter states survive page reloads and browser transitions.
15
+
16
+ ---
17
+
18
+ ## 📦 Minimal Blueprint
19
+
20
+ ### Generic List Attribute Filter (Zero-JS)
21
+ Bind the filter to a container `id`. Target items declare attributes matching the filter key.
22
+ ```html
23
+ <nav data-ln-filter="employees-list">
24
+ <!-- Reset Sentinel -->
25
+ <label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-reset checked> All</label>
26
+ <!-- Values -->
27
+ <label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-value="design"> Design</label>
28
+ <label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-value="dev"> Development</label>
29
+ </nav>
30
+
31
+ <ul id="employees-list">
32
+ <li data-category="design">Ana Petrova — UI Designer</li>
33
+ <li data-category="dev">Marko Nikolov — Developer</li>
34
+ </ul>
35
+ ```
36
+
37
+ ### Table Column Filter with Auto-Population & Persistence
38
+ Auto-populates filter checkboxes from Column Index `2` (Department) and saves state to storage.
39
+ ```html
40
+ <nav id="dept-filter" data-ln-filter="users-table" data-ln-filter-col="2" data-ln-persist>
41
+ <label><input type="checkbox" data-ln-filter-key="dept" data-ln-filter-reset checked> All Departments</label>
42
+ <!-- Checklist generated dynamically here -->
43
+ <template>
44
+ <label><input type="checkbox"> {{ text }}</label>
45
+ </template>
46
+ </nav>
47
+
48
+ <table id="users-table">
49
+ <thead>
50
+ <tr><th>ID</th><th>Name</th><th>Department</th></tr>
51
+ </thead>
52
+ <tbody>
53
+ <tr><td>1</td><td>Ana Petrova</td><td>Engineering</td></tr>
54
+ <tr><td>2</td><td>Marko Nikolov</td><td>Design</td></tr>
55
+ </tbody>
56
+ </table>
57
+ ```
58
+
59
+ ---
60
+
61
+ ## 🛠️ Declarative API Contract
62
+
63
+ ### HTML Attributes
64
+
65
+ | Attribute | Elements | Description |
66
+ | :--- | :--- | :--- |
67
+ | `data-ln-filter` | Container root | Component root. Value is the `id` of the target container whose items are filtered. |
68
+ | `data-ln-filter-key` | `<input type="checkbox">` | The field name representing the target dataset attribute (e.g. `category` matches `data-category`). |
69
+ | `data-ln-filter-value` | `<input type="checkbox">` | The value to match. Active checkboxes show matching items; others are hidden. |
70
+ | `data-ln-filter-reset` | `<input type="checkbox">` | Marks the reset ("All") sentinel. |
71
+ | `data-ln-filter-col` | Container root | Opt-in. 0-based column index to filter plain `<table>` rows by column cell text. |
72
+ | `data-ln-persist` | Container root | Opt-in. Persists active checkbox selections in `localStorage` under `lnf:{id}`. |
73
+ | `data-ln-filter-hide` | Children of target | *State*. Automatically toggled on non-matching elements (`display: none !important`). |
74
+
75
+ ---
76
+
77
+ ## ⚡ DOM Events
78
+
79
+ Events are dispatched on **both** the filter navigation container and the target element (dual dispatch).
80
+
81
+ ### `ln-filter:changed`
82
+ Fired when any filter selection is modified.
83
+ - **Payload (`detail`)**: `{ key: string, values: string[] }` (where `values` lists active filter options).
84
+
85
+ ### `ln-filter:reset`
86
+ Fired when the reset sentinel is activated.
87
+ - **Payload (`detail`)**: `{}`
88
+
89
+ ---
90
+
91
+ ## ⚠️ Common Pitfalls
92
+
93
+ - **Driving State Programmatically Without Events:** Changing `input.checked = true` using JavaScript does not trigger browser `change` listeners. You must explicitly dispatch the event:
94
+ ```javascript
95
+ input.checked = true;
96
+ input.dispatchEvent(new Event('change', { bubbles: true }));
97
+ ```
98
+ - **Missing `id` on Persisted Filters:** The `data-ln-persist` storage key relies on the filter element's ID (e.g. `<nav id="my-filter" data-ln-persist>`). If the ID is missing, the component will fail to initialize persistence.
99
+ - **Filtering Coordinated Tables:** `ln-filter` is designed for static lists or plain native tables. Do not target virtualised components like `ln-table` or `ln-data-table`, which manage their own column filters internally.
@@ -0,0 +1 @@
1
+ (function(){"use strict";function S(r,n,l){r.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:l||{}}))}function D(r,n){if(!r||!n)return r;const l=document.createTreeWalker(r,NodeFilter.SHOW_TEXT);for(;l.nextNode();){const o=l.currentNode;o.textContent.indexOf("{{")!==-1&&(o.textContent=o.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(m,d){return n[d]!==void 0?n[d]:""}))}return r}function B(r,n){if(!document.body){document.addEventListener("DOMContentLoaded",function(){B(r,n)}),console.warn("["+n+'] Script loaded before <body> — add "defer" to your <script> tag');return}r()}function R(r,n,l,o){if(r.nodeType!==1)return;const d=n.indexOf("[")!==-1||n.indexOf(".")!==-1||n.indexOf("#")!==-1?n:"["+n+"]",T=Array.from(r.querySelectorAll(d));r.matches&&r.matches(d)&&T.push(r);for(const y of T)y[l]||(y[l]=new o(y))}function F(r,n,l,o,m={}){const d=m.extraAttributes||[],T=m.onAttributeChange||null,y=m.onInit||null;function A(b){const _=b||document.body;R(_,r,n,l),y&&y(_)}return B(function(){const b=new MutationObserver(function(I){for(let w=0;w<I.length;w++){const f=I[w];if(f.type==="childList")for(let t=0;t<f.addedNodes.length;t++){const e=f.addedNodes[t];e.nodeType===1&&(R(e,r,n,l),y&&y(e))}else f.type==="attributes"&&(T&&f.target[n]?T(f.target,f.attributeName):(R(f.target,r,n,l),y&&y(f.target)))}});let _=[];if(r.indexOf("[")!==-1){const I=/\[([\w-]+)/g;let w;for(;(w=I.exec(r))!==null;)_.push(w[1])}else _.push(r);b.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:_.concat(d)})},o),window[n]=A,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){A(document.body)}):A(document.body),A}const L={};function K(r,n){L[r]=n}function H(r){return L[r]||{ingress:n=>n,egress:n=>n}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=K,window.lnCore.getDataMapper=H);function j(r,n){let l=!1;return function(){l||(l=!0,queueMicrotask(function(){l=!1,r(),n&&n()}))}}const W="ln:";function J(){return location.pathname.replace(/\/+$/,"").toLowerCase()||"/"}function q(r,n){const l=n.getAttribute("data-ln-persist"),o=l!==null&&l!==""?l:n.id;return o?W+r+":"+J()+":"+o:(console.warn('[ln-persist] Element requires id or data-ln-persist="key"',n),null)}function U(r,n){const l=q(r,n);if(!l)return null;try{const o=localStorage.getItem(l);return o!==null?JSON.parse(o):null}catch{return null}}function M(r,n,l){const o=q(r,n);if(o)try{localStorage.setItem(o,JSON.stringify(l))}catch{}}(function(){const r="data-ln-filter",n="lnFilter",l="data-ln-filter-initialized",o="data-ln-filter-key",m="data-ln-filter-value",d="data-ln-filter-hide",T="data-ln-filter-reset",y="data-ln-filter-col",A=new WeakMap;if(window[n]!==void 0)return;function b(t){return t.hasAttribute(T)||t.getAttribute(m)===""}function _(t){let e=null;const i=[];for(let s=0;s<t.inputs.length;s++){const u=t.inputs[s];if(u.checked&&!b(u)){e===null&&(e=u.getAttribute(o));const c=u.getAttribute(m);c&&i.push(c)}}return{key:e,values:i}}function I(t,e){if(t.length!==e.length)return!0;for(let i=0;i<t.length;i++)if(t[i]!==e[i])return!0;return!1}function w(t){const e=t.dom,i=t.colIndex,s=e.querySelector("template");if(!s||i===null)return;const u=document.getElementById(t.targetId);if(!u)return;const c=u.tagName==="TABLE"?u:u.querySelector("table");if(!c||u.hasAttribute("data-ln-table"))return;const h={},a=[],C=c.tBodies;for(let g=0;g<C.length;g++){const p=C[g].rows;for(let v=0;v<p.length;v++){const O=p[v].cells[i],E=O?O.textContent.trim():"";E&&!h[E]&&(h[E]=!0,a.push(E))}}a.sort(function(g,p){return g.localeCompare(p)});const k=e.querySelector("["+o+"]"),x=k?k.getAttribute(o):e.getAttribute("data-ln-filter-key")||"col"+i;for(let g=0;g<a.length;g++){const p=s.content.cloneNode(!0),v=p.querySelector("input");v&&(v.setAttribute(o,x),v.setAttribute(m,a[g]),D(p,{text:a[g]}),e.appendChild(p))}}function f(t){if(t.hasAttribute(l))return this;this.dom=t,this.targetId=t.getAttribute(r);const e=t.getAttribute(y);this.colIndex=e!==null?parseInt(e,10):null,w(this),this.inputs=Array.from(t.querySelectorAll("["+o+"]")),this._filterKey=this.inputs.length>0?this.inputs[0].getAttribute(o):null,this._lastSnapshot=null;const i=this,s=j(function(){i._render()},function(){i._afterRender()});this._queueRender=s,this._attachHandlers();let u=!1;if(t.hasAttribute("data-ln-persist")){const c=U("filter",t);if(c&&c.key&&Array.isArray(c.values)&&c.values.length>0){for(let h=0;h<this.inputs.length;h++){const a=this.inputs[h];b(a)?a.checked=!1:a.getAttribute(o)===c.key&&c.values.indexOf(a.getAttribute(m))!==-1?a.checked=!0:a.checked=!1}s(),u=!0}}if(!u){for(let c=0;c<this.inputs.length;c++)if(this.inputs[c].checked&&!b(this.inputs[c])){s();break}}return t.setAttribute(l,""),this}f.prototype._attachHandlers=function(){const t=this;this.inputs.forEach(function(e){e[n+"Bound"]||(e[n+"Bound"]=!0,e._lnFilterChange=function(){if(b(e)){for(let i=0;i<t.inputs.length;i++)b(t.inputs[i])||(t.inputs[i].checked=!1);e.checked=!0,t._queueRender();return}if(e.checked)for(let i=0;i<t.inputs.length;i++)b(t.inputs[i])&&(t.inputs[i].checked=!1);else{let i=!1;for(let s=0;s<t.inputs.length;s++)if(!b(t.inputs[s])&&t.inputs[s].checked){i=!0;break}if(!i)for(let s=0;s<t.inputs.length;s++)b(t.inputs[s])&&(t.inputs[s].checked=!0)}t._queueRender()},e.addEventListener("change",e._lnFilterChange))})},f.prototype._render=function(){const t=this,e=_(this),i=e.key===null||e.values.length===0,s=[];for(let u=0;u<e.values.length;u++)s.push(e.values[u].toLowerCase());if(t.colIndex!==null)t._filterTableRows(e);else{const u=document.getElementById(t.targetId);if(!u)return;const c=u.children;for(let h=0;h<c.length;h++){const a=c[h];if(i){a.removeAttribute(d);continue}const C=a.getAttribute("data-"+e.key);a.removeAttribute(d),C!==null&&s.indexOf(C.toLowerCase())===-1&&a.setAttribute(d,"true")}}},f.prototype._afterRender=function(){const t=_(this),e=this._lastSnapshot;if(!e||e.key!==t.key||I(e.values,t.values)){this._dispatchOnBoth("ln-filter:changed",{key:t.key,values:t.values.slice()});const s=e&&e.values.length>0,u=t.values.length===0;s&&u&&this._dispatchOnBoth("ln-filter:reset",{}),this._lastSnapshot={key:t.key,values:t.values.slice()}}this.dom.hasAttribute("data-ln-persist")&&(t.key&&t.values.length>0?M("filter",this.dom,{key:t.key,values:t.values.slice()}):M("filter",this.dom,null))},f.prototype._dispatchOnBoth=function(t,e){S(this.dom,t,e);const i=document.getElementById(this.targetId);i&&i!==this.dom&&S(i,t,e)},f.prototype._filterTableRows=function(t){const e=document.getElementById(this.targetId);if(!e)return;const i=e.tagName==="TABLE"?e:e.querySelector("table");if(!i||e.hasAttribute("data-ln-table"))return;const s=t.key||this._filterKey,u=t.values;A.has(i)||A.set(i,{});const c=A.get(i);if(s&&u.length>0){const k=[];for(let x=0;x<u.length;x++)k.push(u[x].toLowerCase());c[s]={col:this.colIndex,values:k}}else s&&delete c[s];const h=Object.keys(c),a=h.length>0,C=i.tBodies;for(let k=0;k<C.length;k++){const x=C[k].rows;for(let g=0;g<x.length;g++){const p=x[g];if(!a){p.removeAttribute(d);continue}let v=!0;for(let O=0;O<h.length;O++){const E=c[h[O]],N=p.cells[E.col],V=N?N.textContent.trim().toLowerCase():"";if(E.values.indexOf(V)===-1){v=!1;break}}v?p.removeAttribute(d):p.setAttribute(d,"true")}}},f.prototype.destroy=function(){if(this.dom[n]){if(this.colIndex!==null){const t=document.getElementById(this.targetId);if(t){const e=t.tagName==="TABLE"?t:t.querySelector("table");if(e&&A.has(e)){const i=A.get(e),s=this._filterKey;s&&i[s]&&delete i[s],Object.keys(i).length===0&&A.delete(e)}}}this.inputs.forEach(function(t){t._lnFilterChange&&(t.removeEventListener("change",t._lnFilterChange),delete t._lnFilterChange),delete t[n+"Bound"]}),this.dom.removeAttribute(l),delete this.dom[n]}},F(r,n,f,"ln-filter")})()})();
@@ -0,0 +1,7 @@
1
+ // ==========================================================================
2
+ // ln-filter — Attribute-based filter component
3
+ // ==========================================================================
4
+
5
+ [data-ln-filter-hide="true"] {
6
+ display: none !important;
7
+ }
@@ -0,0 +1,404 @@
1
+ import { dispatch, fillTemplate, registerComponent } from '../../ln-core';
2
+ import { createBatcher } from '../../ln-core';
3
+ import { persistGet, persistSet } from '../../ln-core';
4
+
5
+ (function () {
6
+ const DOM_SELECTOR = 'data-ln-filter';
7
+ const DOM_ATTRIBUTE = 'lnFilter';
8
+ const INIT_ATTR = 'data-ln-filter-initialized';
9
+ const KEY_ATTR = 'data-ln-filter-key';
10
+ const VALUE_ATTR = 'data-ln-filter-value';
11
+ const HIDE_ATTR = 'data-ln-filter-hide';
12
+ const RESET_ATTR = 'data-ln-filter-reset';
13
+ const COL_ATTR = 'data-ln-filter-col';
14
+
15
+ // Shared column filter state per table (AND across columns, OR within column)
16
+ const _tableFilters = new WeakMap();
17
+
18
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
19
+
20
+ function _isReset(input) {
21
+ return input.hasAttribute(RESET_ATTR) || input.getAttribute(VALUE_ATTR) === '';
22
+ }
23
+
24
+ function _deriveActive(self) {
25
+ let key = null;
26
+ const values = [];
27
+ for (let i = 0; i < self.inputs.length; i++) {
28
+ const input = self.inputs[i];
29
+ if (input.checked && !_isReset(input)) {
30
+ if (key === null) key = input.getAttribute(KEY_ATTR);
31
+ const v = input.getAttribute(VALUE_ATTR);
32
+ if (v) values.push(v);
33
+ }
34
+ }
35
+ return { key: key, values: values };
36
+ }
37
+
38
+ function _arraysDiffer(a, b) {
39
+ if (a.length !== b.length) return true;
40
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return true;
41
+ return false;
42
+ }
43
+
44
+ // ─── Auto-populate from table column ───────────────────────
45
+
46
+ function _populateFromColumn(instance) {
47
+ const dom = instance.dom;
48
+ const colIndex = instance.colIndex;
49
+ const tmpl = dom.querySelector('template');
50
+ if (!tmpl || colIndex === null) return;
51
+
52
+ const target = document.getElementById(instance.targetId);
53
+ if (!target) return;
54
+
55
+ const table = target.tagName === 'TABLE' ? target : target.querySelector('table');
56
+ if (!table || target.hasAttribute('data-ln-table')) return;
57
+
58
+ // Collect unique values from column
59
+ const seen = {};
60
+ const values = [];
61
+ const bodies = table.tBodies;
62
+ for (let b = 0; b < bodies.length; b++) {
63
+ const rows = bodies[b].rows;
64
+ for (let r = 0; r < rows.length; r++) {
65
+ const cell = rows[r].cells[colIndex];
66
+ const text = cell ? cell.textContent.trim() : '';
67
+ if (text && !seen[text]) {
68
+ seen[text] = true;
69
+ values.push(text);
70
+ }
71
+ }
72
+ }
73
+ values.sort(function (a, b) {
74
+ return a.localeCompare(b);
75
+ });
76
+
77
+ // Determine the filter key from existing inputs or dom attribute
78
+ const existingInput = dom.querySelector('[' + KEY_ATTR + ']');
79
+ const filterKey = existingInput
80
+ ? existingInput.getAttribute(KEY_ATTR)
81
+ : (dom.getAttribute('data-ln-filter-key') || 'col' + colIndex);
82
+
83
+ // Clone template for each value
84
+ for (let i = 0; i < values.length; i++) {
85
+ const clone = tmpl.content.cloneNode(true);
86
+ const input = clone.querySelector('input');
87
+ if (!input) continue;
88
+ input.setAttribute(KEY_ATTR, filterKey);
89
+ input.setAttribute(VALUE_ATTR, values[i]);
90
+ fillTemplate(clone, { text: values[i] });
91
+ dom.appendChild(clone);
92
+ }
93
+ }
94
+
95
+ // ─── Component ─────────────────────────────────────────────
96
+
97
+ function _component(dom) {
98
+ if (dom.hasAttribute(INIT_ATTR)) return this;
99
+
100
+ this.dom = dom;
101
+ this.targetId = dom.getAttribute(DOM_SELECTOR);
102
+
103
+ // Column index for table filtering (null = not a table column filter)
104
+ const colAttr = dom.getAttribute(COL_ATTR);
105
+ this.colIndex = colAttr !== null ? parseInt(colAttr, 10) : null;
106
+
107
+ // Auto-populate from table column if template present
108
+ _populateFromColumn(this);
109
+
110
+ // Collect inputs AFTER auto-populate (new inputs may have been added)
111
+ this.inputs = Array.from(dom.querySelectorAll('[' + KEY_ATTR + ']'));
112
+ this._filterKey = this.inputs.length > 0 ? this.inputs[0].getAttribute(KEY_ATTR) : null;
113
+
114
+ // Event-diff cache — null means never dispatched yet
115
+ this._lastSnapshot = null;
116
+
117
+ const self = this;
118
+
119
+ const queueRender = createBatcher(
120
+ function () { self._render(); },
121
+ function () { self._afterRender(); }
122
+ );
123
+
124
+ // Stash for use in change handler
125
+ this._queueRender = queueRender;
126
+
127
+ this._attachHandlers();
128
+
129
+ // ─── Restore persisted filter ─────────────────────────────
130
+ let _persistRestored = false;
131
+ if (dom.hasAttribute('data-ln-persist')) {
132
+ const saved = persistGet('filter', dom);
133
+ if (saved && saved.key && Array.isArray(saved.values) && saved.values.length > 0) {
134
+ // Write input.checked on matching inputs
135
+ for (let i = 0; i < this.inputs.length; i++) {
136
+ const input = this.inputs[i];
137
+ if (_isReset(input)) {
138
+ input.checked = false;
139
+ } else if (input.getAttribute(KEY_ATTR) === saved.key &&
140
+ saved.values.indexOf(input.getAttribute(VALUE_ATTR)) !== -1) {
141
+ input.checked = true;
142
+ } else {
143
+ input.checked = false;
144
+ }
145
+ }
146
+ queueRender();
147
+ _persistRestored = true;
148
+ }
149
+ }
150
+
151
+ if (!_persistRestored) {
152
+ // DOM is already canonical — only schedule render if anything is pre-checked
153
+ // so visibility is applied and the initial ln-filter:changed fires (via
154
+ // null-snapshot diff in _afterRender).
155
+ for (let i = 0; i < this.inputs.length; i++) {
156
+ if (this.inputs[i].checked && !_isReset(this.inputs[i])) {
157
+ queueRender();
158
+ break;
159
+ }
160
+ }
161
+ }
162
+
163
+ dom.setAttribute(INIT_ATTR, '');
164
+ return this;
165
+ }
166
+
167
+ // ─── Handlers ──────────────────────────────────────────────
168
+
169
+ _component.prototype._attachHandlers = function () {
170
+ const self = this;
171
+
172
+ this.inputs.forEach(function (input) {
173
+ if (input[DOM_ATTRIBUTE + 'Bound']) return;
174
+ input[DOM_ATTRIBUTE + 'Bound'] = true;
175
+
176
+ input._lnFilterChange = function () {
177
+ if (_isReset(input)) {
178
+ // Reset sentinel — regardless of direction, enforce checked + clear values
179
+ for (let i = 0; i < self.inputs.length; i++) {
180
+ if (!_isReset(self.inputs[i])) self.inputs[i].checked = false;
181
+ }
182
+ // Force sentinel back to checked (clicking an already-checked sentinel
183
+ // would natively uncheck it; force back to checked)
184
+ input.checked = true;
185
+ self._queueRender();
186
+ return;
187
+ }
188
+
189
+ if (input.checked) {
190
+ // Mutual exclusion: uncheck all reset sentinels
191
+ for (let i = 0; i < self.inputs.length; i++) {
192
+ if (_isReset(self.inputs[i])) self.inputs[i].checked = false;
193
+ }
194
+ } else {
195
+ // If no non-reset values remain checked, fall back to reset
196
+ let anyChecked = false;
197
+ for (let i = 0; i < self.inputs.length; i++) {
198
+ if (!_isReset(self.inputs[i]) && self.inputs[i].checked) {
199
+ anyChecked = true;
200
+ break;
201
+ }
202
+ }
203
+ if (!anyChecked) {
204
+ for (let i = 0; i < self.inputs.length; i++) {
205
+ if (_isReset(self.inputs[i])) self.inputs[i].checked = true;
206
+ }
207
+ }
208
+ }
209
+
210
+ self._queueRender();
211
+ };
212
+ input.addEventListener('change', input._lnFilterChange);
213
+ });
214
+ };
215
+
216
+ // ─── Render ────────────────────────────────────────────────
217
+
218
+ _component.prototype._render = function () {
219
+ const self = this;
220
+ const active = _deriveActive(this);
221
+ const isReset = active.key === null || active.values.length === 0;
222
+
223
+ // Build lowercase lookup for target filtering
224
+ const lowerValues = [];
225
+ for (let i = 0; i < active.values.length; i++) {
226
+ lowerValues.push(active.values[i].toLowerCase());
227
+ }
228
+
229
+ // Apply filter
230
+ if (self.colIndex !== null) {
231
+ // Table column filtering — shared multi-column logic
232
+ self._filterTableRows(active);
233
+ } else {
234
+ // Standard target-children filtering by data attribute
235
+ const target = document.getElementById(self.targetId);
236
+ if (!target) return;
237
+
238
+ const children = target.children;
239
+ for (let i = 0; i < children.length; i++) {
240
+ const el = children[i];
241
+
242
+ if (isReset) {
243
+ el.removeAttribute(HIDE_ATTR);
244
+ continue;
245
+ }
246
+
247
+ const attr = el.getAttribute('data-' + active.key);
248
+ el.removeAttribute(HIDE_ATTR);
249
+
250
+ if (attr === null) continue;
251
+
252
+ // OR logic: visible if attr matches ANY active value
253
+ if (lowerValues.indexOf(attr.toLowerCase()) === -1) {
254
+ el.setAttribute(HIDE_ATTR, 'true');
255
+ }
256
+ }
257
+ }
258
+ };
259
+
260
+ _component.prototype._afterRender = function () {
261
+ const active = _deriveActive(this);
262
+ const prev = this._lastSnapshot;
263
+ const changed = !prev
264
+ || prev.key !== active.key
265
+ || _arraysDiffer(prev.values, active.values);
266
+
267
+ if (changed) {
268
+ // ln-filter:changed always fires when state moves
269
+ this._dispatchOnBoth('ln-filter:changed', {
270
+ key: active.key,
271
+ values: active.values.slice()
272
+ });
273
+
274
+ // ln-filter:reset fires only on transition into reset state
275
+ const wasActive = prev && prev.values.length > 0;
276
+ const nowReset = active.values.length === 0;
277
+ if (wasActive && nowReset) {
278
+ this._dispatchOnBoth('ln-filter:reset', {});
279
+ }
280
+
281
+ this._lastSnapshot = { key: active.key, values: active.values.slice() };
282
+ }
283
+
284
+ // Persist current filter state
285
+ if (this.dom.hasAttribute('data-ln-persist')) {
286
+ if (active.key && active.values.length > 0) {
287
+ persistSet('filter', this.dom, { key: active.key, values: active.values.slice() });
288
+ } else {
289
+ persistSet('filter', this.dom, null);
290
+ }
291
+ }
292
+ };
293
+
294
+ _component.prototype._dispatchOnBoth = function (eventName, detail) {
295
+ dispatch(this.dom, eventName, detail);
296
+ const target = document.getElementById(this.targetId);
297
+ if (target && target !== this.dom) {
298
+ dispatch(target, eventName, detail);
299
+ }
300
+ };
301
+
302
+ // ─── Table Row Filtering ───────────────────────────────────
303
+
304
+ _component.prototype._filterTableRows = function (active) {
305
+ const target = document.getElementById(this.targetId);
306
+ if (!target) return;
307
+
308
+ const table = target.tagName === 'TABLE' ? target : target.querySelector('table');
309
+ if (!table) return;
310
+
311
+ // Guard: don't filter if this is an ln-table (it handles its own filtering)
312
+ if (target.hasAttribute('data-ln-table')) return;
313
+
314
+ const key = active.key || this._filterKey;
315
+ const values = active.values;
316
+
317
+ // Get or create shared filter map for this table
318
+ if (!_tableFilters.has(table)) {
319
+ _tableFilters.set(table, {});
320
+ }
321
+ const filters = _tableFilters.get(table);
322
+
323
+ // Update this filter's entry
324
+ if (key && values.length > 0) {
325
+ const lower = [];
326
+ for (let i = 0; i < values.length; i++) {
327
+ lower.push(values[i].toLowerCase());
328
+ }
329
+ filters[key] = { col: this.colIndex, values: lower };
330
+ } else if (key) {
331
+ delete filters[key];
332
+ }
333
+
334
+ // Check if any column filters are active
335
+ const filterKeys = Object.keys(filters);
336
+ const hasFilters = filterKeys.length > 0;
337
+
338
+ // Apply all active filters to all rows (AND across columns, OR within column)
339
+ const bodies = table.tBodies;
340
+ for (let b = 0; b < bodies.length; b++) {
341
+ const rows = bodies[b].rows;
342
+ for (let r = 0; r < rows.length; r++) {
343
+ const row = rows[r];
344
+
345
+ if (!hasFilters) {
346
+ row.removeAttribute(HIDE_ATTR);
347
+ continue;
348
+ }
349
+
350
+ let visible = true;
351
+ for (let f = 0; f < filterKeys.length; f++) {
352
+ const filter = filters[filterKeys[f]];
353
+ const cell = row.cells[filter.col];
354
+ const cellText = cell ? cell.textContent.trim().toLowerCase() : '';
355
+ // OR within column: visible if cell text matches ANY filter value
356
+ if (filter.values.indexOf(cellText) === -1) {
357
+ visible = false;
358
+ break; // AND across columns: fail fast
359
+ }
360
+ }
361
+
362
+ if (visible) {
363
+ row.removeAttribute(HIDE_ATTR);
364
+ } else {
365
+ row.setAttribute(HIDE_ATTR, 'true');
366
+ }
367
+ }
368
+ }
369
+ };
370
+
371
+ // ─── Destroy ───────────────────────────────────────────────
372
+
373
+ _component.prototype.destroy = function () {
374
+ if (!this.dom[DOM_ATTRIBUTE]) return;
375
+
376
+ // Clean up table filter registry
377
+ if (this.colIndex !== null) {
378
+ const target = document.getElementById(this.targetId);
379
+ if (target) {
380
+ const table = target.tagName === 'TABLE' ? target : target.querySelector('table');
381
+ if (table && _tableFilters.has(table)) {
382
+ const filters = _tableFilters.get(table);
383
+ const key = this._filterKey;
384
+ if (key && filters[key]) delete filters[key];
385
+ if (Object.keys(filters).length === 0) _tableFilters.delete(table);
386
+ }
387
+ }
388
+ }
389
+
390
+ this.inputs.forEach(function (input) {
391
+ if (input._lnFilterChange) {
392
+ input.removeEventListener('change', input._lnFilterChange);
393
+ delete input._lnFilterChange;
394
+ }
395
+ delete input[DOM_ATTRIBUTE + 'Bound'];
396
+ });
397
+ this.dom.removeAttribute(INIT_ATTR);
398
+ delete this.dom[DOM_ATTRIBUTE];
399
+ };
400
+
401
+ // ─── Init ──────────────────────────────────────────────────
402
+
403
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-filter');
404
+ })();