@livenetworks/ashlar 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +189 -177
- package/js/COMPONENTS.md +55 -49
- package/js/index.js +22 -5
- package/js/ln-accordion/README.md +186 -137
- package/js/ln-accordion/ln-accordion.js +1 -1
- package/js/ln-ajax/README.md +73 -4
- package/js/ln-ajax/ln-ajax.js +1 -1
- package/js/ln-ajax/src/ln-ajax.js +13 -13
- package/js/ln-api-connector/README.md +261 -150
- package/js/ln-api-connector/ln-api-connector.js +1 -1
- package/js/ln-api-connector/src/ln-api-connector.js +368 -265
- package/js/ln-api-queue/README.md +219 -0
- package/js/ln-api-queue/ln-api-queue.js +1 -0
- package/js/ln-api-queue/src/ln-api-queue.js +320 -0
- package/js/ln-api-queue/src/queue-storage.js +455 -0
- package/js/ln-autoresize/README.md +47 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -1
- package/js/ln-autosave/README.md +33 -1
- package/js/ln-autosave/ln-autosave.js +1 -1
- package/js/ln-autosave/src/ln-autosave.js +4 -4
- package/js/ln-circular-progress/README.md +27 -13
- package/js/ln-circular-progress/ln-circular-progress.js +1 -1
- package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
- package/js/ln-confirm/README.md +162 -86
- package/js/ln-confirm/_ln-confirm.scss +11 -0
- package/js/ln-confirm/ln-confirm.js +1 -1
- package/js/ln-confirm/src/ln-confirm.js +77 -41
- package/js/ln-core/README.md +896 -0
- package/js/ln-core/hash.js +92 -0
- package/js/ln-core/helpers.js +337 -8
- package/js/ln-core/index.js +4 -2
- package/js/ln-core/persist.js +29 -3
- package/js/ln-core/positioning.js +1 -35
- package/js/ln-core/window-cache.js +213 -0
- package/js/ln-couchdb-connector/README.md +177 -156
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
- package/js/ln-data-coordinator/README.md +536 -165
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
- package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
- package/js/ln-data-store/README.md +241 -94
- package/js/ln-data-store/ln-data-store.js +1 -1
- package/js/ln-data-store/src/ln-data-store.js +694 -699
- package/js/ln-date/README.md +39 -1
- package/js/ln-date/ln-date-dev.scss +14 -0
- package/js/ln-date/ln-date.js +1 -1
- package/js/ln-date/src/ln-date.js +236 -178
- package/js/ln-debug/README.md +58 -0
- package/js/ln-debug/ln-debug.js +1 -0
- package/js/ln-debug/src/ln-debug.js +19 -0
- package/js/ln-dropdown/README.md +31 -8
- package/js/ln-dropdown/ln-dropdown.js +1 -1
- package/js/ln-dropdown/src/ln-dropdown.js +30 -13
- package/js/ln-editor/README.md +274 -0
- package/js/ln-editor/ln-editor.js +1 -0
- package/js/ln-editor/ln-editor.scss +43 -0
- package/js/ln-editor/src/ln-editor.js +619 -0
- package/js/ln-external-links/README.md +44 -5
- package/js/ln-external-links/ln-external-links.js +1 -1
- package/js/ln-fill/README.md +199 -0
- package/js/ln-fill/ln-fill.js +1 -0
- package/js/ln-fill/src/ln-fill.js +87 -0
- package/js/ln-filter/README.md +92 -2
- package/js/ln-filter/ln-filter-dev.scss +8 -0
- package/js/ln-filter/ln-filter.js +1 -1
- package/js/ln-filter/src/ln-filter.js +22 -7
- package/js/ln-form/README.md +212 -49
- package/js/ln-form/ln-form.js +1 -1
- package/js/ln-form/src/ln-form.js +99 -199
- package/js/ln-http/README.md +125 -89
- package/js/ln-http/ln-http.js +1 -1
- package/js/ln-http/src/ln-http.js +32 -13
- package/js/ln-icon/README.md +130 -0
- package/js/ln-icon/ln-icon.js +1 -0
- package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
- package/js/ln-link/README.md +47 -9
- package/js/ln-link/ln-link.js +1 -1
- package/js/ln-link/src/ln-link.js +22 -6
- package/js/ln-list/README.md +129 -0
- package/js/ln-list/ln-list.js +1 -0
- package/js/ln-list/ln-list.scss +28 -0
- package/js/ln-list/src/ln-list.js +1216 -0
- package/js/ln-modal/README.md +185 -154
- package/js/ln-modal/ln-modal-dev.scss +13 -0
- package/js/ln-modal/ln-modal.js +1 -1
- package/js/ln-modal/ln-modal.scss +28 -4
- package/js/ln-modal/src/ln-modal.js +137 -201
- package/js/ln-modal-coordinator/README.md +99 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
- package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
- package/js/ln-nav/README.md +46 -3
- package/js/ln-nav/ln-nav.js +1 -1
- package/js/ln-nav/src/ln-nav.js +92 -127
- package/js/ln-number/README.md +28 -1
- package/js/ln-number/ln-number-dev.scss +13 -0
- package/js/ln-number/ln-number.js +1 -1
- package/js/ln-number/src/ln-number.js +443 -350
- package/js/ln-options/README.md +66 -0
- package/js/ln-options/ln-options.js +1 -0
- package/js/ln-options/src/ln-options.js +74 -0
- package/js/ln-popover/README.md +40 -5
- package/js/ln-popover/ln-popover.js +1 -1
- package/js/ln-popover/src/ln-popover.js +38 -11
- package/js/ln-progress/README.md +413 -442
- package/js/ln-progress/ln-progress.js +1 -1
- package/js/ln-progress/src/ln-progress.js +98 -150
- package/js/ln-router/README.md +432 -0
- package/js/ln-router/ln-router.js +1 -0
- package/js/ln-router/src/ln-router.js +539 -0
- package/js/ln-search/README.md +158 -83
- package/js/ln-search/ln-search-dev.scss +8 -0
- package/js/ln-search/ln-search.js +1 -1
- package/js/ln-search/src/ln-search.js +12 -9
- package/js/ln-slug/README.md +110 -0
- package/js/ln-slug/ln-slug.js +1 -0
- package/js/ln-slug/src/ln-slug.js +74 -0
- package/js/ln-sort/README.md +170 -0
- package/js/ln-sort/ln-sort-dev.scss +8 -0
- package/js/ln-sort/ln-sort.js +1 -0
- package/js/ln-sort/src/ln-sort.js +178 -0
- package/js/ln-sortable/README.md +133 -95
- package/js/ln-sortable/ln-sortable.js +1 -1
- package/js/ln-stat/README.md +55 -0
- package/js/ln-stat/ln-stat.js +1 -0
- package/js/ln-stat/src/ln-stat.js +55 -0
- package/js/ln-table/README.md +285 -101
- package/js/ln-table/ln-table-dev.scss +17 -0
- package/js/ln-table/ln-table.js +1 -1
- package/js/ln-table/ln-table.scss +1 -1
- package/js/ln-table/src/ln-table.js +1493 -473
- package/js/ln-table-coordinator/README.md +73 -0
- package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
- package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
- package/js/ln-tabs/README.md +221 -137
- package/js/ln-tabs/ln-tabs-dev.scss +18 -0
- package/js/ln-tabs/ln-tabs.js +1 -1
- package/js/ln-tabs/src/ln-tabs.js +41 -24
- package/js/ln-time/README.md +14 -0
- package/js/ln-time/ln-time.js +1 -1
- package/js/ln-time/src/ln-time.js +223 -192
- package/js/ln-toast/README.md +178 -122
- package/js/ln-toast/ln-toast.js +1 -15
- package/js/ln-toast/src/ln-toast.js +232 -210
- package/js/ln-toast/template.html +16 -12
- package/js/ln-toggle/README.md +61 -2
- package/js/ln-toggle/ln-toggle-dev.scss +13 -0
- package/js/ln-toggle/ln-toggle.js +1 -1
- package/js/ln-toggle/src/ln-toggle.js +56 -43
- package/js/ln-tooltip/README.md +95 -58
- package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -1
- package/js/ln-tooltip/src/ln-tooltip.js +45 -6
- package/js/ln-translations/README.md +118 -96
- package/js/ln-translations/ln-translations.js +1 -1
- package/js/ln-translations/src/ln-translations.js +10 -5
- package/js/ln-upload/README.md +85 -10
- package/js/ln-upload/ln-upload.js +1 -1
- package/js/ln-upload/src/ln-upload.js +26 -10
- package/js/ln-validate/README.md +72 -0
- package/js/ln-validate/ln-validate-dev.scss +15 -0
- package/js/ln-validate/ln-validate.js +1 -1
- package/js/ln-validate/src/ln-validate.js +64 -0
- package/package.json +61 -56
- package/scss/base/_global.scss +2 -2
- package/scss/base/_typography.scss +1 -2
- package/scss/components/_date.scss +1 -0
- package/scss/components/_dropdown.scss +0 -5
- package/scss/components/_editor.scss +5 -0
- package/scss/components/_filter.scss +26 -25
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +13 -3
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +36 -23
- package/scss/components/_toast.scss +68 -41
- package/scss/components/_toggle.scss +16 -0
- package/scss/config/_density.scss +205 -117
- package/scss/config/_icons.scss +10 -9
- package/scss/config/_tokens.scss +69 -47
- package/scss/config/mixins/_accordion.scss +2 -7
- package/scss/config/mixins/_alert.scss +4 -4
- package/scss/config/mixins/_breadcrumbs.scss +2 -2
- package/scss/config/mixins/_btn.scss +1 -1
- package/scss/config/mixins/_card.scss +89 -14
- package/scss/config/mixins/_collapsible.scss +3 -0
- package/scss/config/mixins/_colors.scss +1 -1
- package/scss/config/mixins/_diagnostics.scss +11 -0
- package/scss/config/mixins/_display.scss +1 -0
- package/scss/config/mixins/_dropdown.scss +3 -5
- package/scss/config/mixins/_editor.scss +124 -0
- package/scss/config/mixins/_empty-state.scss +7 -0
- package/scss/config/mixins/_filter.scss +3 -5
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +2 -0
- package/scss/config/mixins/_ln-table.scss +41 -2
- package/scss/config/mixins/_modal.scss +149 -66
- package/scss/config/mixins/_nav.scss +3 -4
- package/scss/config/mixins/_popover.scss +39 -0
- package/scss/config/mixins/_sidebar.scss +1 -1
- package/scss/config/mixins/_sort.scss +42 -0
- package/scss/config/mixins/_table.scss +85 -60
- package/scss/config/mixins/_toast.scss +179 -152
- package/scss/config/mixins/_tooltip.scss +17 -7
- package/scss/config/mixins/_typography.scss +4 -2
- package/scss/ln-ashlar-dev.scss +27 -9
- package/scss/ln-ashlar.scss +7 -1
- package/js/ln-data-table/README.md +0 -110
- package/js/ln-data-table/ln-data-table.js +0 -1
- package/js/ln-data-table/ln-data-table.scss +0 -10
- package/js/ln-data-table/src/ln-data-table.js +0 -1087
- package/js/ln-icons/README.md +0 -88
- package/js/ln-icons/ln-icons.js +0 -1
- package/js/ln-table/ln-table-sort.js +0 -1
- package/js/ln-table/src/ln-table-sort.js +0 -156
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function p(n,o,f){n.dispatchEvent(new CustomEvent(o,{bubbles:!0,detail:f||{}}))}function _(n,o,f){const a=new CustomEvent(o,{bubbles:!0,cancelable:!0,detail:f||{}});return n.dispatchEvent(a),a}function w(n,o){if(!document.body){document.addEventListener("DOMContentLoaded",function(){w(n,o)}),console.warn("["+o+'] Script loaded before <body> — add "defer" to your <script> tag');return}n()}function g(n,o,f,a){if(n.nodeType!==1)return;const i=o.indexOf("[")!==-1||o.indexOf(".")!==-1||o.indexOf("#")!==-1?o:"["+o+"]",t=Array.from(n.querySelectorAll(i));n.matches&&n.matches(i)&&t.push(n);for(const e of t)e[f]||(e[f]=new a(e))}function C(n,o,f,a,m={}){const i=m.extraAttributes||[],t=m.onAttributeChange||null,e=m.onInit||null;function l(r){const c=r||document.body;g(c,n,o,f),e&&e(c)}return w(function(){const r=new MutationObserver(function(d){for(let u=0;u<d.length;u++){const s=d[u];if(s.type==="childList"){for(let h=0;h<s.addedNodes.length;h++){const b=s.addedNodes[h];b.nodeType===1&&(g(b,n,o,f),e&&e(b))}for(let h=0;h<s.removedNodes.length;h++){const b=s.removedNodes[h];if(b.nodeType===1){const x=n.indexOf("[")!==-1||n.indexOf(".")!==-1||n.indexOf("#")!==-1?n:"["+n+"]",v=Array.from(b.querySelectorAll(x));b.matches&&b.matches(x)&&v.push(b);for(let y=0;y<v.length;y++){const A=v[y];if(!document.contains(A)){const E=A[o];E&&typeof E.destroy=="function"&&E.destroy()}}}}}else s.type==="attributes"&&(t&&s.target[o]?t(s.target,s.attributeName):(g(s.target,n,o,f),e&&e(s.target)))}});let c=[];if(n.indexOf("[")!==-1){const d=/\[([\w-]+)/g;let u;for(;(u=d.exec(n))!==null;)c.push(u[1])}else c.push(n);r.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:c.concat(i)})},a),window[o]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const L={};function O(n,o){L[n]=o}function D(n){return L[n]||{ingress:o=>o,egress:o=>o}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=O,window.lnCore.getDataMapper=D),(function(){const n="data-ln-sortable",o="lnSortable",f="data-ln-sortable-handle";if(window[o]!==void 0)return;function a(i){this.dom=i,this.isEnabled=i.getAttribute(n)!=="disabled",this._dragging=null,i.setAttribute("aria-roledescription","sortable list");const t=this;return this._onPointerDown=function(e){t.isEnabled&&t._handlePointerDown(e)},i.addEventListener("pointerdown",this._onPointerDown),this}a.prototype.enable=function(){this.isEnabled||this.dom.setAttribute(n,"")},a.prototype.disable=function(){this.isEnabled&&this.dom.setAttribute(n,"disabled")},a.prototype.destroy=function(){this.dom[o]&&(this.dom.removeEventListener("pointerdown",this._onPointerDown),p(this.dom,"ln-sortable:destroyed",{target:this.dom}),delete this.dom[o])},a.prototype._handlePointerDown=function(i){let t=i.target.closest("["+f+"]"),e;if(t){for(e=t;e&&e.parentElement!==this.dom;)e=e.parentElement;if(!e||e.parentElement!==this.dom)return}else{if(this.dom.querySelector("["+f+"]"))return;for(e=i.target;e&&e.parentElement!==this.dom;)e=e.parentElement;if(!e||e.parentElement!==this.dom)return;t=e}const r=Array.from(this.dom.children).indexOf(e);if(_(this.dom,"ln-sortable:before-drag",{item:e,index:r}).defaultPrevented)return;i.preventDefault(),t.setPointerCapture(i.pointerId),this._dragging=e,e.classList.add("ln-sortable--dragging"),e.setAttribute("aria-grabbed","true"),this.dom.classList.add("ln-sortable--active"),p(this.dom,"ln-sortable:drag-start",{item:e,index:r});const d=this,u=function(h){d._handlePointerMove(h)},s=function(h){d._handlePointerEnd(h),t.removeEventListener("pointermove",u),t.removeEventListener("pointerup",s),t.removeEventListener("pointercancel",s)};t.addEventListener("pointermove",u),t.addEventListener("pointerup",s),t.addEventListener("pointercancel",s)},a.prototype._handlePointerMove=function(i){if(!this._dragging)return;const t=Array.from(this.dom.children),e=this._dragging;for(const l of t)l.classList.remove("ln-sortable--drop-before","ln-sortable--drop-after");for(const l of t){if(l===e)continue;const r=l.getBoundingClientRect(),c=r.top+r.height/2;if(i.clientY>=r.top&&i.clientY<c){l.classList.add("ln-sortable--drop-before");break}else if(i.clientY>=c&&i.clientY<=r.bottom){l.classList.add("ln-sortable--drop-after");break}}},a.prototype._handlePointerEnd=function(i){if(!this._dragging)return;const t=this._dragging,e=Array.from(this.dom.children),l=e.indexOf(t);let r=null,c=null;for(const d of e){if(d.classList.contains("ln-sortable--drop-before")){r=d,c="before";break}if(d.classList.contains("ln-sortable--drop-after")){r=d,c="after";break}}for(const d of e)d.classList.remove("ln-sortable--drop-before","ln-sortable--drop-after");if(t.classList.remove("ln-sortable--dragging"),t.removeAttribute("aria-grabbed"),this.dom.classList.remove("ln-sortable--active"),r&&r!==t){c==="before"?this.dom.insertBefore(t,r):this.dom.insertBefore(t,r.nextElementSibling);const u=Array.from(this.dom.children).indexOf(t);p(this.dom,"ln-sortable:reordered",{item:t,oldIndex:l,newIndex:u})}this._dragging=null};function m(i){const t=i[o];if(!t)return;const e=i.getAttribute(n)!=="disabled";e!==t.isEnabled&&(t.isEnabled=e,p(i,e?"ln-sortable:enabled":"ln-sortable:disabled",{target:i}))}C(n,o,a,"ln-sortable",{onAttributeChange:m})})()})();
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...e){typeof e[0]=="string"&&(e[0].startsWith("[ln-")||e[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,e)}}const g={};function q(t,e){g[t]||(g[t]=document.querySelector('[data-ln-template="'+t+'"]'));const r=g[t];return r?r.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function m(t,e,r){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:r||{}}))}function M(t,e,r){const d=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:r||{}});return t.dispatchEvent(d),d}function L(t,e){if(!t||!e)return t;const r=t.querySelectorAll("[data-ln-field]");for(let n=0;n<r.length;n++){const o=r[n],l=o.getAttribute("data-ln-field");e[l]!=null&&(o.textContent=e[l])}const d=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<d.length;n++){const o=d[n],l=o.getAttribute("data-ln-attr").split(",");for(let s=0;s<l.length;s++){const a=l[s].trim().split(":");if(a.length!==2)continue;const c=a[0].trim(),u=a[1].trim();e[u]!=null&&o.setAttribute(c,e[u])}}const f=t.querySelectorAll("[data-ln-show]");for(let n=0;n<f.length;n++){const o=f[n],l=o.getAttribute("data-ln-show");l in e&&o.classList.toggle("hidden",!e[l])}const i=t.querySelectorAll("[data-ln-class]");for(let n=0;n<i.length;n++){const o=i[n],l=o.getAttribute("data-ln-class").split(",");for(let s=0;s<l.length;s++){const a=l[s].trim().split(":");if(a.length!==2)continue;const c=a[0].trim(),u=a[1].trim();u in e&&o.classList.toggle(c,!!e[u])}}return t}function P(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const r=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let d=0;d<r.length;d++)r[d].dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));return t}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(t){if(!(!t.target.matches||!t.target.matches("[data-ln-fillable]")))if(t.detail)L(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let r=0;r<e.length;r++)e[r].textContent=""}})));function A(t,e){if(!t||!e)return t;const r=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;r.nextNode();){const i=r.currentNode;i.textContent.indexOf("{{")!==-1&&(i.textContent=i.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,o){return e[o]!==void 0?e[o]:""}))}const d=function(i,n){return e[n]!==void 0?e[n]:""},f=Array.from(t.querySelectorAll("*"));t.nodeType===1&&f.push(t);for(let i=0;i<f.length;i++){const n=f[i],o=n.attributes;for(let l=0;l<o.length;l++){const s=o[l];s.value.indexOf("{{")!==-1&&n.setAttribute(s.name,s.value.replace(/\{\{\s*(\w+)\s*\}\}/g,d))}}return t}function I(t,e,r,d,f,i){const n={};for(let l=0;l<t.children.length;l++){const s=t.children[l],a=s.getAttribute("data-ln-key");a&&(n[a]=s)}const o=document.createDocumentFragment();for(let l=0;l<e.length;l++){const s=e[l],a=String(d(s));let c=n[a];if(c)f(c,s,l);else{const u=q(r,i);if(!u||(A(u,s),c=u.firstElementChild,!c))continue;c.setAttribute("data-ln-key",a),f(c,s,l)}o.appendChild(c)}t.textContent="",t.appendChild(o)}function x(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){x(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function w(t,e,r,d){if(t.nodeType!==1)return;const i=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(i));t.matches&&t.matches(i)&&n.push(t);for(const o of n)o[r]||(o[r]=new d(o))}function N(t,e,r,d,f={}){const i=f.extraAttributes||[],n=f.onAttributeChange||null,o=f.onInit||null;function l(s){const a=s||document.body;w(a,t,e,r),o&&o(a)}return x(function(){const s=new MutationObserver(function(c){for(let u=0;u<c.length;u++){const h=c[u];if(h.type==="childList"){for(let p=0;p<h.addedNodes.length;p++){const b=h.addedNodes[p];b.nodeType===1&&(w(b,t,e,r),o&&o(b))}for(let p=0;p<h.removedNodes.length;p++){const b=h.removedNodes[p];if(b.nodeType===1){const D=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",C=Array.from(b.querySelectorAll(D));b.matches&&b.matches(D)&&C.push(b);for(let v=0;v<C.length;v++){const T=C[v];if(!document.contains(T)){const E=T[e];E&&typeof E.destroy=="function"&&E.destroy()}}}}}else h.type==="attributes"&&(n&&h.target[e]?n(h.target,h.attributeName):(w(h.target,t,e,r),o&&o(h.target)))}});let a=[];if(t.indexOf("[")!==-1){const c=/\[([\w-]+)/g;let u;for(;(u=c.exec(t))!==null;)a.push(u[1])}else a.push(t);s.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:a.concat(i)})},d),window[e]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const _={};function B(t,e){_[t]=e}function j(t){return _[t]||{ingress:e=>e,egress:e=>e}}const S={};function k(t,e){if(!t||typeof e!="object")return;const r=t.toLowerCase().split("-")[0];S[r]=e}function W(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return S[e]||null}k("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=B,window.lnCore.getDataMapper=j,window.lnCore.registerLocaleFallback=k,window.lnCore.getLocaleFallback=W,window.lnCore.fillTemplate=A,window.lnCore.fill=L,window.lnCore.lnFill=P,window.lnCore.renderList=I);function O(t){return(t||"").replace(/^#/,"")}function y(t){const e=t===void 0?location.hash:t,r={},d=O(e);if(!d)return r;const f=d.split("&");for(let i=0;i<f.length;i++){const n=f[i];if(!n)continue;const o=n.indexOf(":"),l=o>-1?n.slice(0,o):n,s=o>-1?n.slice(o+1):"";if(l)try{r[l]=decodeURIComponent(s)}catch{r[l]=s}}return r}function F(t){if(!t)return null;const e=y();return t in e?e[t]:null}function R(t,e){if(!t)return;const r=y();e==null?delete r[t]:r[t]=String(e);const f=Object.keys(r).map(function(i){const n=r[i];return n===""?i:i+":"+encodeURIComponent(n)}).join("&");O(location.hash)!==f&&(location.hash=f)}function K(t){return t.button===1||t.ctrlKey||t.metaKey||t.shiftKey?!1:(t.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=y,window.lnCore.hashGet=F,window.lnCore.hashSet=R,window.lnCore.hashLinkClick=K),(function(){const t="data-ln-sortable",e="lnSortable",r="data-ln-sortable-handle";if(window[e]!==void 0)return;function d(i){this.dom=i,this.isEnabled=i.getAttribute(t)!=="disabled",this._dragging=null,i.setAttribute("aria-roledescription","sortable list");const n=this;return this._onPointerDown=function(o){n.isEnabled&&n._handlePointerDown(o)},i.addEventListener("pointerdown",this._onPointerDown),this}d.prototype.enable=function(){this.isEnabled||this.dom.setAttribute(t,"")},d.prototype.disable=function(){this.isEnabled&&this.dom.setAttribute(t,"disabled")},d.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("pointerdown",this._onPointerDown),m(this.dom,"ln-sortable:destroyed",{target:this.dom}),delete this.dom[e])},d.prototype._handlePointerDown=function(i){let n=i.target.closest("["+r+"]"),o;if(n){for(o=n;o&&o.parentElement!==this.dom;)o=o.parentElement;if(!o||o.parentElement!==this.dom)return}else{if(this.dom.querySelector("["+r+"]"))return;for(o=i.target;o&&o.parentElement!==this.dom;)o=o.parentElement;if(!o||o.parentElement!==this.dom)return;n=o}const s=Array.from(this.dom.children).indexOf(o);if(M(this.dom,"ln-sortable:before-drag",{item:o,index:s}).defaultPrevented)return;i.preventDefault(),n.setPointerCapture(i.pointerId),this._dragging=o,o.classList.add("ln-sortable--dragging"),o.setAttribute("aria-grabbed","true"),this.dom.classList.add("ln-sortable--active"),m(this.dom,"ln-sortable:drag-start",{item:o,index:s});const c=this,u=function(p){c._handlePointerMove(p)},h=function(p){c._handlePointerEnd(p),n.removeEventListener("pointermove",u),n.removeEventListener("pointerup",h),n.removeEventListener("pointercancel",h)};n.addEventListener("pointermove",u),n.addEventListener("pointerup",h),n.addEventListener("pointercancel",h)},d.prototype._handlePointerMove=function(i){if(!this._dragging)return;const n=Array.from(this.dom.children),o=this._dragging;for(const l of n)l.classList.remove("ln-sortable--drop-before","ln-sortable--drop-after");for(const l of n){if(l===o)continue;const s=l.getBoundingClientRect(),a=s.top+s.height/2;if(i.clientY>=s.top&&i.clientY<a){l.classList.add("ln-sortable--drop-before");break}else if(i.clientY>=a&&i.clientY<=s.bottom){l.classList.add("ln-sortable--drop-after");break}}},d.prototype._handlePointerEnd=function(i){if(!this._dragging)return;const n=this._dragging,o=Array.from(this.dom.children),l=o.indexOf(n);let s=null,a=null;for(const c of o){if(c.classList.contains("ln-sortable--drop-before")){s=c,a="before";break}if(c.classList.contains("ln-sortable--drop-after")){s=c,a="after";break}}for(const c of o)c.classList.remove("ln-sortable--drop-before","ln-sortable--drop-after");if(n.classList.remove("ln-sortable--dragging"),n.removeAttribute("aria-grabbed"),this.dom.classList.remove("ln-sortable--active"),s&&s!==n){a==="before"?this.dom.insertBefore(n,s):this.dom.insertBefore(n,s.nextElementSibling);const u=Array.from(this.dom.children).indexOf(n);m(this.dom,"ln-sortable:reordered",{item:n,oldIndex:l,newIndex:u})}this._dragging=null};function f(i){const n=i[e];if(!n)return;const o=i.getAttribute(t)!=="disabled";o!==n.isEnabled&&(n.isEnabled=o,m(i,o?"ln-sortable:enabled":"ln-sortable:disabled",{target:i}))}N(t,e,d,"ln-sortable",{onAttributeChange:f})})()})();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# ln-stat
|
|
2
|
+
|
|
3
|
+
Displays a live count from an `ln-data-store` via the `ln-data-coordinator` binder. Updates automatically on every store mutation. Opt-in: add `data-ln-stat="<storeName>"` to any inline element.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Attributes
|
|
8
|
+
|
|
9
|
+
| Attribute | Element | Description |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `data-ln-stat="<storeName>"` | Any inline element | Store name to bind. |
|
|
12
|
+
| `data-ln-stat-filter="<field>:<value>"` | Same element | Optional single-field filter. Format: `field:value`. The value is String-matched against the store's `_filter` (treated as an array `[value]`). Multi-field filters are out of scope for v1. |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Events
|
|
17
|
+
|
|
18
|
+
### Dispatched
|
|
19
|
+
- `ln-stat:request-count` `{ stat: storeName, filters }` — fired on init (bubbles to coordinator).
|
|
20
|
+
|
|
21
|
+
### Received
|
|
22
|
+
- `ln-stat:set-count` `{ count }` — coordinator delivers this; component sets `textContent` and removes `is-loading` class.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Example
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<div data-ln-data-coordinator>
|
|
30
|
+
<div data-ln-data-store="tenants"></div>
|
|
31
|
+
<div data-ln-api-connector data-ln-api-endpoint="/api/tenants"></div>
|
|
32
|
+
|
|
33
|
+
<!-- Total count -->
|
|
34
|
+
<strong data-ln-stat="tenants"></strong> tenants
|
|
35
|
+
|
|
36
|
+
<!-- Filtered count -->
|
|
37
|
+
<strong data-ln-stat="tenants" data-ln-stat-filter="active:true" class="is-loading"></strong> active
|
|
38
|
+
</div>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The `is-loading` class is removed on first delivery — add it to show a loading placeholder.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Notes
|
|
46
|
+
|
|
47
|
+
- ln-stat never imports or reads `ln-data-store` directly — it is a pure event client.
|
|
48
|
+
- The coordinator re-serves `ln-stat:set-count` on every store mutation.
|
|
49
|
+
- `data-ln-stat-filter` supports one `field:value` pair. The value is passed as `[value]` (array) to `store.count(filters)`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 🔧 Internals
|
|
54
|
+
|
|
55
|
+
Source: `js/ln-stat/ln-stat.js`. The coordinator's `_serveStat` method handles `ln-stat:request-count`: reads `data-ln-stat` from the dispatching element, guards with `_ownsStore(name)`, calls `store.count(filters)` (`filters` from `e.detail.filters`), and dispatches `ln-stat:set-count { count }` back on the element. On every store mutation, the coordinator's `_refreshAll()` re-parses `data-ln-stat-filter` from each stat element and re-calls `store.count()` — the component itself holds no count state between deliveries.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...e){typeof e[0]=="string"&&(e[0].startsWith("[ln-")||e[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,e)}}const w={};function q(t,e){w[t]||(w[t]=document.querySelector('[data-ln-template="'+t+'"]'));const o=w[t];return o?o.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function D(t,e,o){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:o||{}}))}function A(t,e){if(!t||!e)return t;const o=t.querySelectorAll("[data-ln-field]");for(let n=0;n<o.length;n++){const i=o[n],l=i.getAttribute("data-ln-field");e[l]!=null&&(i.textContent=e[l])}const u=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<u.length;n++){const i=u[n],l=i.getAttribute("data-ln-attr").split(",");for(let c=0;c<l.length;c++){const d=l[c].trim().split(":");if(d.length!==2)continue;const a=d[0].trim(),f=d[1].trim();e[f]!=null&&i.setAttribute(a,e[f])}}const r=t.querySelectorAll("[data-ln-show]");for(let n=0;n<r.length;n++){const i=r[n],l=i.getAttribute("data-ln-show");l in e&&i.classList.toggle("hidden",!e[l])}const s=t.querySelectorAll("[data-ln-class]");for(let n=0;n<s.length;n++){const i=s[n],l=i.getAttribute("data-ln-class").split(",");for(let c=0;c<l.length;c++){const d=l[c].trim().split(":");if(d.length!==2)continue;const a=d[0].trim(),f=d[1].trim();f in e&&i.classList.toggle(a,!!e[f])}}return t}function N(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const o=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let u=0;u<o.length;u++)o[u].dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));return t}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(t){if(!(!t.target.matches||!t.target.matches("[data-ln-fillable]")))if(t.detail)A(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let o=0;o<e.length;o++)e[o].textContent=""}})));function L(t,e){if(!t||!e)return t;const o=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;o.nextNode();){const s=o.currentNode;s.textContent.indexOf("{{")!==-1&&(s.textContent=s.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,i){return e[i]!==void 0?e[i]:""}))}const u=function(s,n){return e[n]!==void 0?e[n]:""},r=Array.from(t.querySelectorAll("*"));t.nodeType===1&&r.push(t);for(let s=0;s<r.length;s++){const n=r[s],i=n.attributes;for(let l=0;l<i.length;l++){const c=i[l];c.value.indexOf("{{")!==-1&&n.setAttribute(c.name,c.value.replace(/\{\{\s*(\w+)\s*\}\}/g,u))}}return t}function M(t,e,o,u,r,s){const n={};for(let l=0;l<t.children.length;l++){const c=t.children[l],d=c.getAttribute("data-ln-key");d&&(n[d]=c)}const i=document.createDocumentFragment();for(let l=0;l<e.length;l++){const c=e[l],d=String(u(c));let a=n[d];if(a)r(a,c,l);else{const f=q(o,s);if(!f||(L(f,c),a=f.firstElementChild,!a))continue;a.setAttribute("data-ln-key",d),r(a,c,l)}i.appendChild(a)}t.textContent="",t.appendChild(i)}function S(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){S(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function g(t,e,o,u){if(t.nodeType!==1)return;const s=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(s));t.matches&&t.matches(s)&&n.push(t);for(const i of n)i[o]||(i[o]=new u(i))}function j(t,e,o,u,r={}){const s=r.extraAttributes||[],n=r.onAttributeChange||null,i=r.onInit||null;function l(c){const d=c||document.body;g(d,t,e,o),i&&i(d)}return S(function(){const c=new MutationObserver(function(a){for(let f=0;f<a.length;f++){const h=a[f];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const p=h.addedNodes[m];p.nodeType===1&&(g(p,t,e,o),i&&i(p))}for(let m=0;m<h.removedNodes.length;m++){const p=h.removedNodes[m];if(p.nodeType===1){const k=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",b=Array.from(p.querySelectorAll(k));p.matches&&p.matches(k)&&b.push(p);for(let C=0;C<b.length;C++){const T=b[C];if(!document.contains(T)){const x=T[e];x&&typeof x.destroy=="function"&&x.destroy()}}}}}else h.type==="attributes"&&(n&&h.target[e]?n(h.target,h.attributeName):(g(h.target,t,e,o),i&&i(h.target)))}});let d=[];if(t.indexOf("[")!==-1){const a=/\[([\w-]+)/g;let f;for(;(f=a.exec(t))!==null;)d.push(f[1])}else d.push(t);c.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:d.concat(s)})},u),window[e]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const v={};function F(t,e){v[t]=e}function W(t){return v[t]||{ingress:e=>e,egress:e=>e}}const _={};function O(t,e){if(!t||typeof e!="object")return;const o=t.toLowerCase().split("-")[0];_[o]=e}function I(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return _[e]||null}O("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=F,window.lnCore.getDataMapper=W,window.lnCore.registerLocaleFallback=O,window.lnCore.getLocaleFallback=I,window.lnCore.fillTemplate=L,window.lnCore.fill=A,window.lnCore.lnFill=N,window.lnCore.renderList=M);function E(t){return(t||"").replace(/^#/,"")}function y(t){const e=t===void 0?location.hash:t,o={},u=E(e);if(!u)return o;const r=u.split("&");for(let s=0;s<r.length;s++){const n=r[s];if(!n)continue;const i=n.indexOf(":"),l=i>-1?n.slice(0,i):n,c=i>-1?n.slice(i+1):"";if(l)try{o[l]=decodeURIComponent(c)}catch{o[l]=c}}return o}function B(t){if(!t)return null;const e=y();return t in e?e[t]:null}function K(t,e){if(!t)return;const o=y();e==null?delete o[t]:o[t]=String(e);const r=Object.keys(o).map(function(s){const n=o[s];return n===""?s:s+":"+encodeURIComponent(n)}).join("&");E(location.hash)!==r&&(location.hash=r)}function R(t){return t.button===1||t.ctrlKey||t.metaKey||t.shiftKey?!1:(t.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=y,window.lnCore.hashGet=B,window.lnCore.hashSet=K,window.lnCore.hashLinkClick=R),(function(){const t="data-ln-stat",e="lnStat";if(window[e]!==void 0)return;function o(r){if(!r)return null;const s=r.indexOf(":");if(s===-1)return null;const n=r.slice(0,s),i=r.slice(s+1),l={};return l[n]=[i],l}function u(r){return this.dom=r,this._storeName=r.getAttribute(t),this._filters=o(r.getAttribute("data-ln-stat-filter")),this._onSetCount=function(s){r.textContent=String(s.detail.count),r.classList.remove("is-loading")},r.addEventListener("ln-stat:set-count",this._onSetCount),D(r,"ln-stat:request-count",{stat:this._storeName,filters:this._filters}),this}u.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("ln-stat:set-count",this._onSetCount),delete this.dom[e])},j(t,e,u,"ln-stat")})()})();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { registerComponent, dispatch } from '../../ln-core';
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
const DOM_SELECTOR = 'data-ln-stat';
|
|
5
|
+
const DOM_ATTRIBUTE = 'lnStat';
|
|
6
|
+
|
|
7
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
8
|
+
|
|
9
|
+
// ─── Filter Parser ─────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
function _parseFilter(raw) {
|
|
12
|
+
if (!raw) return null;
|
|
13
|
+
const colonIdx = raw.indexOf(':');
|
|
14
|
+
if (colonIdx === -1) return null;
|
|
15
|
+
const field = raw.slice(0, colonIdx);
|
|
16
|
+
const val = raw.slice(colonIdx + 1);
|
|
17
|
+
const filters = {};
|
|
18
|
+
filters[field] = [val];
|
|
19
|
+
return filters;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Component ─────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function _component(dom) {
|
|
25
|
+
this.dom = dom;
|
|
26
|
+
this._storeName = dom.getAttribute(DOM_SELECTOR);
|
|
27
|
+
this._filters = _parseFilter(dom.getAttribute('data-ln-stat-filter'));
|
|
28
|
+
|
|
29
|
+
const self = this;
|
|
30
|
+
|
|
31
|
+
this._onSetCount = function (e) {
|
|
32
|
+
dom.textContent = String(e.detail.count);
|
|
33
|
+
dom.classList.remove('is-loading');
|
|
34
|
+
};
|
|
35
|
+
dom.addEventListener('ln-stat:set-count', this._onSetCount);
|
|
36
|
+
|
|
37
|
+
// Request initial count from coordinator
|
|
38
|
+
dispatch(dom, 'ln-stat:request-count', {
|
|
39
|
+
stat: this._storeName,
|
|
40
|
+
filters: this._filters
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_component.prototype.destroy = function () {
|
|
47
|
+
if (!this.dom[DOM_ATTRIBUTE]) return;
|
|
48
|
+
this.dom.removeEventListener('ln-stat:set-count', this._onSetCount);
|
|
49
|
+
delete this.dom[DOM_ATTRIBUTE];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ─── Init ──────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-stat');
|
|
55
|
+
})();
|
package/js/ln-table/README.md
CHANGED
|
@@ -1,101 +1,285 @@
|
|
|
1
|
-
# ln-table
|
|
2
|
-
|
|
3
|
-
A zero-dependency, high-performance
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 🧭
|
|
8
|
-
|
|
9
|
-
1.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
1
|
+
# ln-table
|
|
2
|
+
|
|
3
|
+
A zero-dependency, high-performance table presenter component that supports both Server-Rendered (SSR) Mode and Data-Driven Mode in `ln-ashlar`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🧭 Philosophical Modes
|
|
8
|
+
|
|
9
|
+
1. **Server-Rendered (SSR) Mode**:
|
|
10
|
+
Hydrates standard, backend-printed HTML tables in-place. Reads the existing `<tbody>` rows once on load and layers sorting, filtering, text searches, and scroll virtualization on top of pre-rendered markup.
|
|
11
|
+
2. **Data-Driven Mode**:
|
|
12
|
+
Operates as a dynamic presenter engine. Clones templates, interpolates double-curly braces (`{{ field }}`), manages checkbox selection lists, updates footers, and coordinates dataset requests via AJAX coordinators.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🏛️ Layer 1 vs Layer 2 Architecture
|
|
17
|
+
|
|
18
|
+
`ln-table` strictly operates as a **Layer 1 Component** (Data & DOM presenter):
|
|
19
|
+
- Manages internal data arrays, virtual scroll rendering, sliding window cache, row template cloning, cell formatting, and selection state.
|
|
20
|
+
- Responds to command/request events (`ln-table:set-search`, `ln-table:set-filter`, `ln-table:set-data`, `ln-table:set-loading`, `ln-table:request-clear-filters`).
|
|
21
|
+
- Emits lifecycle notifications (`ln-table:ready`, `ln-table:rendered`, `ln-table:filter`, `ln-table:sorted`, `ln-table:select`).
|
|
22
|
+
|
|
23
|
+
External UI controls (`ln-search`, `ln-filter`, filter header buttons, clear buttons, keyboard focus shortcuts) are orchestrated by **Layer 2 Coordinator** [`ln-table-coordinator`](../ln-table-coordinator/README.md).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 📦 Minimal Blueprint
|
|
28
|
+
|
|
29
|
+
### 1. SSR / Markup Mode
|
|
30
|
+
```html
|
|
31
|
+
<div id="employees-table" data-ln-table>
|
|
32
|
+
<template data-ln-table-empty>
|
|
33
|
+
<article class="ln-table__empty-state">
|
|
34
|
+
<h3>No matches found</h3>
|
|
35
|
+
<button type="button" data-ln-table-clear>Clear filters</button>
|
|
36
|
+
</article>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<table>
|
|
40
|
+
<thead>
|
|
41
|
+
<tr>
|
|
42
|
+
<th>Name
|
|
43
|
+
<ul data-ln-sort="employees-table"><li><button type="button" data-ln-sort-dir="asc" aria-label="Sort ascending"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrows-sort"></use></svg></button></li><li><button type="button" data-ln-sort-dir="desc" aria-label="Sort descending"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrow-up"></use></svg></button></li><li><button type="button" data-ln-sort-dir="none" aria-label="Remove sort"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrow-down"></use></svg></button></li></ul>
|
|
44
|
+
<!-- Icon mapping is state-based, not direction-based — see js/ln-sort/README.md "Icon convention". -->
|
|
45
|
+
</th>
|
|
46
|
+
<th>Salary</th>
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody>
|
|
50
|
+
<tr>
|
|
51
|
+
<td>Dalibor Sojic</td>
|
|
52
|
+
<td data-ln-value="120000">$120,000</td>
|
|
53
|
+
</tr>
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
</div>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Data-Driven Mode
|
|
60
|
+
```html
|
|
61
|
+
<section data-ln-table="products" data-ln-table-source="products" id="products-table">
|
|
62
|
+
<table>
|
|
63
|
+
<thead>
|
|
64
|
+
<tr>
|
|
65
|
+
<th data-ln-table-col="name">Product Name</th>
|
|
66
|
+
<th data-ln-table-col="category">Category</th>
|
|
67
|
+
</tr>
|
|
68
|
+
</thead>
|
|
69
|
+
<tbody data-ln-table-body></tbody>
|
|
70
|
+
</table>
|
|
71
|
+
|
|
72
|
+
<!-- Row Template -->
|
|
73
|
+
<template data-ln-template="products-row">
|
|
74
|
+
<tr data-ln-table-row>
|
|
75
|
+
<td>{{ name }}</td>
|
|
76
|
+
<td>{{ category }}</td>
|
|
77
|
+
</tr>
|
|
78
|
+
</template>
|
|
79
|
+
</section>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
> Row templates support `{{ field }}` (text) and
|
|
83
|
+
> `data-ln-table-cell-attr="field:attr"` (attributes) only — both stamped once
|
|
84
|
+
> at clone time. `data-ln-field` is **not** processed in rows (the row
|
|
85
|
+
> pipeline never calls `fill()`); it would sit inert in the DOM.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 🛠️ Attributes Reference
|
|
90
|
+
|
|
91
|
+
| Attribute | Elements | Description |
|
|
92
|
+
| :--- | :--- | :--- |
|
|
93
|
+
| `data-ln-table` | Root wrapper | Component identifier. Target must carry a unique `id`. |
|
|
94
|
+
| `data-ln-table-source` | Root wrapper | Opt-in indicator for Data-Driven Mode. |
|
|
95
|
+
| `data-ln-table-selectable` | Root wrapper | Enables checkbox-based row selections. |
|
|
96
|
+
| `data-ln-table-window="N"` | Root wrapper | Opt-in server-side sliding-window virtualization. `N` sets the resident-row cap (default 1000). Requires Data-Driven Mode. Observable: add/remove toggles windowed mode ON/OFF live; changing `N` while windowed reconfigures the live cache. |
|
|
97
|
+
| ~~`data-ln-table-search`~~ | — | **Removed.** Drive the search input with `data-ln-search="<tableId>"` — `ln-table` consumes `ln-search:change` directly in SSR mode only; data-driven mode relies on `ln-table-coordinator` to translate it into `ln-table:set-search`. |
|
|
98
|
+
| `data-ln-table-col="field"` | `<th>` | Maps column header to data object field keys. |
|
|
99
|
+
| `data-ln-value` | `<td>` | Raw machine value behind a formatted cell — sorting/filtering operate on this, not the displayed text. Read via `ln-core.readValue`. |
|
|
100
|
+
| `data-ln-sort` | `<ul>` inside `<th>` | Sort control — see [`ln-sort`](../ln-sort/README.md). Omit `data-ln-sort-field` on SSR columns (index fallback); set it on data-driven columns (must match `data-ln-table-col`). |
|
|
101
|
+
| `data-ln-table-col-filter` | `<button>` | JS id hook — identifies the filter button in a `<th>`. Pair with `data-ln-popover-for` to open the filter popover. |
|
|
102
|
+
| `data-ln-table-col-select` | `<th>` | Header checkbox column selector. |
|
|
103
|
+
| `data-ln-table-row` | `<tr>` | Target row container in row templates. |
|
|
104
|
+
| `data-ln-table-row-select` | `<input>` | Selection checkbox in row templates. |
|
|
105
|
+
| `data-ln-table-row-action="name"`| `<button>` | Action button trigger in row templates. |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## ⚡ DOM Events
|
|
110
|
+
|
|
111
|
+
### Emitted Events
|
|
112
|
+
|
|
113
|
+
- **`ln-table:ready`** `{ total }`
|
|
114
|
+
Fired after the initial DOM rows are parsed.
|
|
115
|
+
- **`ln-table:request-data`** `{ table, sort, filters, search }`
|
|
116
|
+
Requests a fresh dataset when sort, filter, or search is changed. Windowed mode (`data-ln-table-window`) adds `{ offset, limit, queryGen }`.
|
|
117
|
+
- **`ln-table:rendered`** `{ table, total, visible }`
|
|
118
|
+
Fired after a dynamic template render finishes drawing.
|
|
119
|
+
- **`ln-table:row-click`** `{ table, id, record }`
|
|
120
|
+
Fired when clicking on row contents.
|
|
121
|
+
- **`ln-table:row-action`** `{ table, id, action, record }`
|
|
122
|
+
Fired when clicking row buttons.
|
|
123
|
+
Note: the row-action click handler calls `e.stopPropagation()`, so a `data-ln-table-row-action` button cannot be combined with document-delegated triggers like `data-ln-modal-for` or `data-ln-fill-*` on the same button — the click never reaches those listeners. Pick one mechanism per button.
|
|
124
|
+
|
|
125
|
+
### Received Events
|
|
126
|
+
|
|
127
|
+
- **`ln-table:set-data`** `{ data, total, filtered, filterOptions }`
|
|
128
|
+
Applies the payload array and triggers rendering. Windowed mode expects `{ offset, queryGen }` echoed back — routed straight into the internal window cache.
|
|
129
|
+
- **`ln-table:set-loading`** `{ loading }`
|
|
130
|
+
Toggles the visual loading dimmed state overlay.
|
|
131
|
+
- **`ln-table:page-failed`** `{ offset }`
|
|
132
|
+
Windowed mode: the coordinator reports the page fetch at `offset` failed. Releases it from the cache's in-flight set — no auto-retry; the next `ensure()` (scroll, filter, resize) requests it again.
|
|
133
|
+
- **`ln-table:request-revalidate`**
|
|
134
|
+
Windowed mode: the coordinator asks the cache to re-fetch the currently visible page after a local mutation. Stale rows stay visible until the response lands; does not jump back to page 0.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Column Filters
|
|
139
|
+
|
|
140
|
+
Column filters use static authored markup — a `[data-ln-popover]` block containing `[data-ln-filter]` checkboxes. `ln-filter` dispatches `ln-filter:changed`; `ln-table` consumes the coordinator's translated `ln-table:set-filter` instead.
|
|
141
|
+
|
|
142
|
+
### What ln-table does
|
|
143
|
+
|
|
144
|
+
`ln-table` never listens for `ln-filter:changed` itself — no such binding exists in either mode. `ln-filter` dual-dispatches `ln-filter:changed` on both its own `<ul data-ln-filter>` root and directly on the table element; `ln-table-coordinator` receives it there and translates it into the commands `ln-table` actually listens for:
|
|
145
|
+
|
|
146
|
+
1. `ln-table-coordinator` maps `e.detail.key` to a column via `data-ln-table-filter-col` on `<th>` and toggles `.ln-filter-active` on the funnel `<button>` (the dot indicator).
|
|
147
|
+
2. `ln-table-coordinator` dispatches `ln-table:set-filter` at the table.
|
|
148
|
+
3. `ln-table` stores the active filter values in `_columnFilters`.
|
|
149
|
+
4. **SSR mode**: runs `_applyFilterAndSort()` + `_render()` in-memory.
|
|
150
|
+
5. **Data-driven mode**: calls `_requestData()`.
|
|
151
|
+
6. Dispatches `ln-table:filter`.
|
|
152
|
+
|
|
153
|
+
An SSR table using column filters still requires the `[data-ln-table-coordinator]` wrapper — SSR self-binding only covers sort and search (see "Sort integration" below), not filters or clear-all.
|
|
154
|
+
|
|
155
|
+
### What ln-table does NOT do
|
|
156
|
+
|
|
157
|
+
- Does not generate filter dropdown markup.
|
|
158
|
+
- Does not track which options exist in the dataset.
|
|
159
|
+
- Does not handle mutual exclusion of checkboxes (`ln-filter` owns that).
|
|
160
|
+
|
|
161
|
+
### Markup contract
|
|
162
|
+
|
|
163
|
+
```html
|
|
164
|
+
<!-- th attribute maps filter key → column -->
|
|
165
|
+
<th data-ln-table-filter-col="department">
|
|
166
|
+
Department
|
|
167
|
+
<button class="table-filter" type="button"
|
|
168
|
+
data-ln-table-col-filter
|
|
169
|
+
data-ln-popover-for="filter-dept"
|
|
170
|
+
aria-label="Filter department">
|
|
171
|
+
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-filter"></use></svg>
|
|
172
|
+
</button>
|
|
173
|
+
</th>
|
|
174
|
+
|
|
175
|
+
<!-- Popover: sibling to [data-ln-table], not inside it -->
|
|
176
|
+
<div data-ln-popover id="filter-dept">
|
|
177
|
+
<input type="search" data-ln-search="filter-dept-list" data-ln-search-items="label" placeholder="Search...">
|
|
178
|
+
<ul id="filter-dept-list" data-ln-filter="my-table">
|
|
179
|
+
<li><label><input type="checkbox" data-ln-filter-key="department" data-ln-filter-reset checked> All</label></li>
|
|
180
|
+
<li><label><input type="checkbox" data-ln-filter-key="department" data-ln-filter-value="Engineering"> Engineering</label></li>
|
|
181
|
+
</ul>
|
|
182
|
+
</div>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Options come from the domain (backend enum or lookup), never derived from the dataset. Include options that may have zero matches — they represent valid domain states.
|
|
186
|
+
|
|
187
|
+
### Clear-all
|
|
188
|
+
|
|
189
|
+
**SSR mode:** `data-ln-table-clear` on a button inside the table wrapper. Clears search term and all `[data-ln-filter]` containers targeting this table.
|
|
190
|
+
|
|
191
|
+
**Data-driven mode:** `data-ln-table-clear-all` on a button. Resets `currentFilters`, clears visual indicators on all filter buttons, and calls `_requestData()`.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Windowed Mode
|
|
196
|
+
|
|
197
|
+
Opt-in server-side sliding-window virtualization for datasets too large to cache client-side. Add `data-ln-table-window="N"` to a Data-Driven Mode table; the component owns a bounded `ln-core.createWindowCache` instance instead of the full dataset.
|
|
198
|
+
|
|
199
|
+
### What it does
|
|
200
|
+
|
|
201
|
+
1. Caches at most `N` rows (default 1000), LRU-evicting the least-recently-touched rows once the cap is exceeded.
|
|
202
|
+
2. Extends `ln-table:request-data` with `{ offset, limit, queryGen }` on every scroll pass that needs new rows.
|
|
203
|
+
3. Reads `{ offset, queryGen }` off `ln-table:set-data` and routes it into the window cache, dropping the response if `queryGen` no longer matches the current query.
|
|
204
|
+
4. Renders blank placeholder rows (no shimmer) for logical rows not yet resident.
|
|
205
|
+
5. Hides the select-all checkbox (D4) — per-row selection still works and survives eviction.
|
|
206
|
+
|
|
207
|
+
### Live observability
|
|
208
|
+
|
|
209
|
+
`data-ln-table-window`, `data-ln-table-window-page`, `data-ln-table-window-threshold`, and `data-ln-table-count` are all observed and apply live, without re-init. Adding `data-ln-table-window` to an initialized, non-windowed, data-driven table enables windowing live — the cache seeds from resident rows, honoring `data-ln-table-count` if present. Removing it disables windowing live: the cache is destroyed and the table issues a fresh full `ln-table:request-data` (no `offset`/`limit`) to repopulate the complete dataset before rendering non-windowed again.
|
|
210
|
+
|
|
211
|
+
### What it does NOT do
|
|
212
|
+
|
|
213
|
+
- Does not cache the full dataset client-side — that's the non-windowed Data-Driven path.
|
|
214
|
+
- Does not paginate — scrolling stays continuous; `offset`/`limit` address server-side pages, not user-facing "Page 2" navigation.
|
|
215
|
+
- Does not retry a dropped stale response — the next `ensure()` pass re-requests it under the current `queryGen`.
|
|
216
|
+
|
|
217
|
+
### Markup contract
|
|
218
|
+
|
|
219
|
+
```html
|
|
220
|
+
<section data-ln-table="documents" data-ln-table-source="documents" data-ln-table-window="1000" id="documents-table">
|
|
221
|
+
...
|
|
222
|
+
</section>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## 🔧 Internals
|
|
228
|
+
|
|
229
|
+
Source: `js/ln-table/ln-table.js`. Sort is a separate, co-loaded component — see `js/ln-sort/ln-sort.js`. Imports from `ln-core`: `cloneTemplateScoped`, `dispatch`, `fill`, `fillTemplate`, `registerComponent`, `createWindowCache`, `readValue`, `detectValueType`, `compareValues`.
|
|
230
|
+
|
|
231
|
+
### Mode detection & lifecycle
|
|
232
|
+
|
|
233
|
+
- The constructor branches once, at construction: `this.isDataDriven = dom.hasAttribute('data-ln-table-source')`.
|
|
234
|
+
- **SSR**: reads `<tbody>` rows once at bootstrap, caches them as static HTML strings in `_data`, and re-sorts/filters that in-memory cache on `ln-search:change` / `ln-sort:change`. Column-filter values arrive as `ln-table:set-filter` from `ln-table-coordinator`, not as a direct `ln-filter:changed` listener.
|
|
235
|
+
- **Data-driven**: `isLoaded = false` until the first `ln-table:set-data`. If `<tbody>` already has rows (hybrid), they're parsed synchronously first — instant local sort/filter/search response before the authoritative dataset arrives. When the background sync lands, `_vStart`/`_vEnd` reset to `-1` and virtual scroll re-initializes against the full dataset.
|
|
236
|
+
- **Loading dimming**: `ln-table:set-loading {loading:true}` adds `.ln-table--loading` to the wrapper; `ln-table:set-data` clears it automatically.
|
|
237
|
+
|
|
238
|
+
### DOM mutations
|
|
239
|
+
|
|
240
|
+
| Phase | Mutation |
|
|
241
|
+
|---|---|
|
|
242
|
+
| `set-loading` | `.ln-table--loading` on the wrapper |
|
|
243
|
+
| `set-data` / sort / filter / search | `<tbody>` rows re-rendered (cloned templates in data-driven mode, cached HTML in SSR); footer counters (`data-ln-table-total` / `-filtered` / `-selected`) updated |
|
|
244
|
+
| sort click | `data-ln-sort-state` on the `[data-ln-sort]` element, owned entirely by `ln-sort` — `ln-table` never touches it |
|
|
245
|
+
| filter change | `.ln-filter-active` toggled on the column's filter button |
|
|
246
|
+
|
|
247
|
+
### Sort integration (`ln-sort`)
|
|
248
|
+
|
|
249
|
+
Sort is fully owned by the standalone [`ln-sort`](../ln-sort/README.md) component — `ln-table`
|
|
250
|
+
only listens for its `ln-sort:change` event on itself, in both modes:
|
|
251
|
+
|
|
252
|
+
- **SSR** — `e.preventDefault()`, re-sorts the in-memory `_data` cache using `row.values[colIndex]`
|
|
253
|
+
(raw `readValue` per cell, type inferred once via `ln-core.detectValueType`), re-renders, keeps
|
|
254
|
+
emitting `ln-table:sorted`.
|
|
255
|
+
- **Data-driven** — `e.preventDefault()`, sets `currentSort = { field, direction }` (or `null` on
|
|
256
|
+
`direction: 'none'`), calls `_requestData()`.
|
|
257
|
+
|
|
258
|
+
`ln-table` never writes `data-ln-sort-state` and never persists sort state — both are `ln-sort`'s
|
|
259
|
+
own responsibility (`data-ln-persist` goes on the `[data-ln-sort]` element itself, not the table
|
|
260
|
+
wrapper — see `js/ln-sort/README.md`).
|
|
261
|
+
|
|
262
|
+
### Search integration (`ln-search`)
|
|
263
|
+
|
|
264
|
+
SSR mode also self-binds `ln-search:change` directly on itself, mirroring `ln-sort:change` above — an SSR table with a `[data-ln-search="<its own id>"]` input works standalone, no coordinator required. Data-driven mode relies on `ln-table-coordinator` translating `ln-search:change` into `ln-table:set-search` — do not wrap an SSR table in `[data-ln-table-coordinator]`, it will double-process search.
|
|
265
|
+
|
|
266
|
+
### MutationObserver flow (`ln-table.js`)
|
|
267
|
+
|
|
268
|
+
A single observer on `document.body`: new `data-ln-table` root injected → `new lnTable(node)`; elements injected inside a live root → local rescan (no full re-init); `data-ln-table-source`/`-window` attribute set on an existing element → re-evaluated live (see Windowed Mode below).
|
|
269
|
+
|
|
270
|
+
### Windowed cache mechanics
|
|
271
|
+
|
|
272
|
+
Backed by `ln-core.createWindowCache` — the table owns the cache instance, not the full dataset:
|
|
273
|
+
|
|
274
|
+
- LRU eviction by touch-recency (not viewport distance) once resident rows exceed `windowSize`.
|
|
275
|
+
- `_renderWindowed()` calls `cache.ensure(startRow, endRow)` on every scroll pass; unresident logical rows render as blank placeholder `<tr>`s (no shimmer) until their page arrives.
|
|
276
|
+
- `requestPage` dispatches `ln-table:request-data` with `{offset, limit, queryGen}`; `cache.ingest()` consumes the matching `ln-table:set-data {offset, queryGen}` and splices the page in. A response whose `queryGen` doesn't match the cache's current generation is dropped — the guard against a stale sort/filter/search response landing after a newer query superseded it.
|
|
277
|
+
- Live reconfiguration routes through `configure()`/`setGrandTotal()`: enabling `data-ln-table-window` on an initialized non-windowed table seeds the cache from resident rows (honoring `data-ln-table-count` if present, else the resident count); removing it destroys the cache and re-issues a full `ln-table:request-data` (no `offset`/`limit`) to repopulate non-windowed.
|
|
278
|
+
|
|
279
|
+
### Filter options — `{value, label}` shape
|
|
280
|
+
|
|
281
|
+
`filterOptions` in `ln-table:set-data` accepts per-field arrays mixing plain strings (`'Draft'` — used as both raw value and label) and `{value, label}` objects (`{value:'true', label:'Active'}` — label renders in the dropdown, raw `value` is echoed verbatim in `ln-table:request-data` filters). This lets a column filter on a raw boolean/enum field (`data-ln-table-col="active"`) while displaying a human-readable computed field in the row template (`{{ status_display }}`) — no app-side label↔value translation needed.
|
|
282
|
+
|
|
283
|
+
### Sticky header/footer
|
|
284
|
+
|
|
285
|
+
`@include ln-table-toolbar` / `@include ln-table-footer` mixins (in `scss/components/_ln-table.scss`) pin the header/footer flush at the scroll container's bounds, with fallback rules targeting direct-child `[data-ln-table] > header` / `> footer`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
@use '../../scss/config/mixins' as *;
|
|
2
|
+
|
|
3
|
+
[data-ln-debug] {
|
|
4
|
+
// Table Integration: missing ID warning
|
|
5
|
+
// Warns if a table wrapper [data-ln-table] is missing an ID, which breaks search/filter integration
|
|
6
|
+
[data-ln-table]:not([id]) {
|
|
7
|
+
@include dev-dom-error("[data-ln-table] is missing a required 'id' attribute for filter mapping.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Legacy sort markup warning
|
|
11
|
+
// These hooks are read only by the retired ln-table-sort.js — ln-table.js ignores them
|
|
12
|
+
th[data-ln-table-sort],
|
|
13
|
+
th:has([data-ln-table-col-sort]),
|
|
14
|
+
th:has(.table-sort) {
|
|
15
|
+
@include dev-inline-error("LEGACY SORT MARKUP — use [data-ln-sort]");
|
|
16
|
+
}
|
|
17
|
+
}
|