@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,88 @@
1
+ # ln-icons
2
+
3
+ A zero-dependency, local-first **On-Demand SVG Sprite Generator** that dynamically monitors, fetches, and compiles SVG icons at runtime.
4
+
5
+ Instead of bundling thousands of heavy vector paths or requiring complex manual build steps, it intercepts standard DOM `<use>` tags, fetches vector definitions from a remote CDN, caches them in `localStorage`, and injects them into a single unified hidden SVG sprite sheet.
6
+
7
+ ---
8
+
9
+ ## 🧭 Philosophy & Architecture
10
+
11
+ 1. **Declarative On-Demand Rendering:** Icons are declared directly in HTML. The component monitors the DOM via `MutationObserver` for `<use>` references with `#ln-` and `#lnc-` prefixes. It only fetches and compiles icons that are actively present on the page.
12
+ 2. **Dual-Prefix Routing:**
13
+ - **`#ln-{name}`**: Automatically routes to the [Tabler Icons](https://tabler.io/icons) library fetched from a public CDN. No configuration required.
14
+ - **`#lnc-{name}`**: Routes to a custom corporate CDN defined via global window settings.
15
+ 3. **Local Caching Layer:** Fetched SVG path structures are instantly cached in `localStorage` under `lni:{id}`. Subsequent visits render icons instantly with zero network roundtrips.
16
+
17
+ ---
18
+
19
+ ## 📦 Minimal Blueprint
20
+
21
+ ### Native Tabler Icon
22
+ ```html
23
+ <svg class="ln-icon" aria-hidden="true">
24
+ <use href="#ln-home"></use>
25
+ </svg>
26
+ ```
27
+
28
+ ### Custom Asset Icon
29
+ Define your custom CDN endpoint before importing the library:
30
+ ```html
31
+ <script>
32
+ window.LN_ICONS_CUSTOM_CDN = "https://cdn.mycompany.com/assets/icons";
33
+ </script>
34
+ <script src="dist/ln-ashlar.iife.js" defer></script>
35
+
36
+ <!-- Renders icon from your custom CDN -->
37
+ <svg class="ln-icon" aria-hidden="true">
38
+ <use href="#lnc-corporate-logo"></use>
39
+ </svg>
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 🛠️ Declarative API Contract
45
+
46
+ ### CSS Utility Classes
47
+
48
+ Configure icon sizes and alignments using standard CSS classes:
49
+
50
+ | Class | Size | Description |
51
+ | :--- | :--- | :--- |
52
+ | `ln-icon` | `1.25rem` | Base styles, sets `fill: none`, `stroke: currentColor`, inherits color. |
53
+ | `ln-icon--sm` | `1rem` | Small icon, designed for inline text badges or buttons. |
54
+ | `ln-icon--lg` | `1.5rem` | Large icon, designed for toolbar buttons. |
55
+ | `ln-icon--xl` | `4rem` | Extra-large icon, designed for empty state illustrations. |
56
+ | `ln-chevron` | — | Automatically rotates `90deg` when an ancestor `.is-active` class is toggled. |
57
+
58
+ ### Global Configuration (`window`)
59
+
60
+ Configure these properties before script initialization:
61
+
62
+ | Variable | Default | Description |
63
+ | :--- | :--- | :--- |
64
+ | `LN_ICONS_CDN` | `https://cdn.jsdelivr.net/npm/@tabler/icons@3.31.0/icons/outline` | Base CDN URL for Tabler Icons. |
65
+ | `LN_ICONS_CUSTOM_CDN` | `null` | Base CDN URL for custom `#lnc-` prefixed SVG resources. |
66
+
67
+ ---
68
+
69
+ ## ⚡ Dynamic Interaction Flow
70
+
71
+ ### Automated Mutation Observability
72
+ The loader observes the DOM continuously. When new content is injected (e.g. by `ln-ajax` or `ln-store`), any new icon `<use>` tag is intercepted, resolved, and rendered.
73
+
74
+ ### Dynamic Attribute Swaps
75
+ Modifying the `href` attribute of a `<use>` element dynamically via JavaScript triggers automatic resolution of the new target icon:
76
+ ```javascript
77
+ const useElement = document.querySelector('use');
78
+ // Dynamically fetches and switches the icon to a checkmark
79
+ useElement.setAttribute('href', '#ln-check');
80
+ ```
81
+
82
+ ---
83
+
84
+ ## ⚠️ Common Pitfalls
85
+
86
+ - **Forgetting `ln-icon` Class:** Standard SVGs default to `100%` width/height. Failing to include the `ln-icon` class will cause the icon to blow up to full viewport size.
87
+ - **Incorrect Prefix Configuration:** Forgetting to define `window.LN_ICONS_CUSTOM_CDN` when using `#lnc-` will cause the loader to fail silently with undefined endpoint errors.
88
+ - **Omitting `aria-hidden="true"`:** Screen readers attempt to read SVG nodes. Always decorate decorative icons with `aria-hidden="true"`, or include an `aria-label` on their parent button.
@@ -0,0 +1 @@
1
+ (function(){"use strict";(function(){const h="ln-icons-sprite",a="#ln-",o="#lnc-",d=new Set,l=new Set;let c=null;const C=(window.LN_ICONS_CDN||"https://cdn.jsdelivr.net/npm/@tabler/icons@3.31.0/icons/outline").replace(/\/$/,""),g=(window.LN_ICONS_CUSTOM_CDN||"").replace(/\/$/,""),u="lni:",m="lni:v",S="1";function v(){try{if(localStorage.getItem(m)!==S){for(let t=localStorage.length-1;t>=0;t--){const n=localStorage.key(t);n&&n.indexOf(u)===0&&localStorage.removeItem(n)}localStorage.setItem(m,S)}}catch{}}v();function N(){return c||(c=document.getElementById(h),c||(c=document.createElementNS("http://www.w3.org/2000/svg","svg"),c.id=h,c.setAttribute("hidden",""),c.setAttribute("aria-hidden","true"),c.appendChild(document.createElementNS("http://www.w3.org/2000/svg","defs")),document.body.insertBefore(c,document.body.firstChild))),c}function A(t){return t.indexOf(o)===0?g+"/"+t.slice(o.length)+".svg":C+"/"+t.slice(a.length)+".svg"}function w(t,n){const e=n.match(/viewBox="([^"]+)"/),i=e?e[1]:"0 0 24 24",s=n.match(/<svg[^>]*>([\s\S]*?)<\/svg>/i),I=s?s[1].trim():"",y=n.match(/<svg([^>]*)>/i),O=y?y[1]:"",r=document.createElementNS("http://www.w3.org/2000/svg","symbol");r.id=t,r.setAttribute("viewBox",i),["fill","stroke","stroke-width","stroke-linecap","stroke-linejoin"].forEach(function(_){const b=O.match(new RegExp(_+'="([^"]*)"'));b&&r.setAttribute(_,b[1])}),r.innerHTML=I,N().querySelector("defs").appendChild(r)}function f(t){if(d.has(t)||l.has(t)||t.indexOf(o)===0&&!g)return;const n=t.slice(1);try{const e=localStorage.getItem(u+n);if(e){w(n,e),d.add(t);return}}catch{}l.add(t),fetch(A(t)).then(function(e){if(!e.ok)throw new Error(e.status);return e.text()}).then(function(e){w(n,e),d.add(t),l.delete(t);try{localStorage.setItem(u+n,e)}catch{}}).catch(function(){l.delete(t)})}function E(t){const n='use[href^="'+a+'"], use[href^="'+o+'"]',e=t.querySelectorAll?t.querySelectorAll(n):[];if(t.matches&&t.matches(n)){const i=t.getAttribute("href");i&&f(i)}Array.prototype.forEach.call(e,function(i){const s=i.getAttribute("href");s&&f(s)})}function p(){E(document),new MutationObserver(function(t){t.forEach(function(n){if(n.type==="childList")n.addedNodes.forEach(function(e){e.nodeType===1&&E(e)});else if(n.type==="attributes"&&n.attributeName==="href"){const e=n.target.getAttribute("href");e&&(e.indexOf(a)===0||e.indexOf(o)===0)&&f(e)}})}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["href"]})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",p):p()})()})();
@@ -0,0 +1,169 @@
1
+ // SVG icon loader — fetches icons on demand, builds a hidden sprite.
2
+ // Fetched SVGs are cached in localStorage (prefix "lni:") so subsequent
3
+ // page loads resolve instantly without network requests.
4
+ //
5
+ // Two prefixes:
6
+ // #ln-{name} → Tabler CDN (https://cdn.jsdelivr.net/npm/@tabler/icons@3.31.0/icons/outline)
7
+ // #lnc-{name} → Custom CDN (window.LN_ICONS_CUSTOM_CDN)
8
+ //
9
+ // Config — set on window BEFORE this script loads:
10
+ // window.LN_ICONS_CDN = 'https://...' (override Tabler CDN base)
11
+ // window.LN_ICONS_CUSTOM_CDN = 'https://...' (required for lnc- icons)
12
+ //
13
+ // Cache: bump CACHE_VERSION to invalidate all cached icons.
14
+ //
15
+ // Usage:
16
+ // <svg class="ln-icon" aria-hidden="true"><use href="#ln-home"></use></svg>
17
+ // <svg class="ln-icon" aria-hidden="true"><use href="#lnc-file-pdf"></use></svg>
18
+
19
+ (function () {
20
+ const SPRITE_ID = 'ln-icons-sprite';
21
+ const PREFIX_LN = '#ln-';
22
+ const PREFIX_LNC = '#lnc-';
23
+
24
+ const loaded = new Set();
25
+ const pending = new Set();
26
+ let spriteEl = null;
27
+
28
+ // Pinned version for cache stability — bump explicitly when upgrading. Override: window.LN_ICONS_CDN
29
+ const BASE_CDN = (window.LN_ICONS_CDN || 'https://cdn.jsdelivr.net/npm/@tabler/icons@3.31.0/icons/outline').replace(/\/$/, '');
30
+ const CUSTOM_CDN = (window.LN_ICONS_CUSTOM_CDN || '').replace(/\/$/, '');
31
+
32
+ const CACHE_PREFIX = 'lni:';
33
+ const CACHE_VER_KEY = 'lni:v';
34
+ const CACHE_VERSION = '1';
35
+
36
+ function _checkCacheVersion() {
37
+ try {
38
+ if (localStorage.getItem(CACHE_VER_KEY) !== CACHE_VERSION) {
39
+ for (let i = localStorage.length - 1; i >= 0; i--) {
40
+ const key = localStorage.key(i);
41
+ if (key && key.indexOf(CACHE_PREFIX) === 0) localStorage.removeItem(key);
42
+ }
43
+ localStorage.setItem(CACHE_VER_KEY, CACHE_VERSION);
44
+ }
45
+ } catch (e) { /* localStorage unavailable or full — proceed without cache */ }
46
+ }
47
+
48
+ _checkCacheVersion();
49
+
50
+ function _getSprite() {
51
+ if (!spriteEl) {
52
+ spriteEl = document.getElementById(SPRITE_ID);
53
+ if (!spriteEl) {
54
+ spriteEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
55
+ spriteEl.id = SPRITE_ID;
56
+ spriteEl.setAttribute('hidden', '');
57
+ spriteEl.setAttribute('aria-hidden', 'true');
58
+ spriteEl.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'defs'));
59
+ document.body.insertBefore(spriteEl, document.body.firstChild);
60
+ }
61
+ }
62
+ return spriteEl;
63
+ }
64
+
65
+ function _url(href) {
66
+ if (href.indexOf(PREFIX_LNC) === 0) return CUSTOM_CDN + '/' + href.slice(PREFIX_LNC.length) + '.svg';
67
+ return BASE_CDN + '/' + href.slice(PREFIX_LN.length) + '.svg';
68
+ }
69
+
70
+ function _addSymbol(id, raw) {
71
+ const viewBoxMatch = raw.match(/viewBox="([^"]+)"/);
72
+ const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 24 24';
73
+ const innerMatch = raw.match(/<svg[^>]*>([\s\S]*?)<\/svg>/i);
74
+ const inner = innerMatch ? innerMatch[1].trim() : '';
75
+ const attrsMatch = raw.match(/<svg([^>]*)>/i);
76
+ const rawAttrs = attrsMatch ? attrsMatch[1] : '';
77
+
78
+ const sym = document.createElementNS('http://www.w3.org/2000/svg', 'symbol');
79
+ sym.id = id;
80
+ sym.setAttribute('viewBox', viewBox);
81
+
82
+ ['fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'].forEach(function (attr) {
83
+ const m = rawAttrs.match(new RegExp(attr + '="([^"]*)"'));
84
+ if (m) sym.setAttribute(attr, m[1]);
85
+ });
86
+
87
+ // Trust boundary: SVG content fetched from pinned CDN version (see BASE_CDN) or localStorage cache
88
+ sym.innerHTML = inner;
89
+ _getSprite().querySelector('defs').appendChild(sym);
90
+ }
91
+
92
+ function _load(href) {
93
+ if (loaded.has(href) || pending.has(href)) return;
94
+ if (href.indexOf(PREFIX_LNC) === 0 && !CUSTOM_CDN) return;
95
+
96
+ const id = href.slice(1);
97
+
98
+ // Check localStorage first
99
+ try {
100
+ const cached = localStorage.getItem(CACHE_PREFIX + id);
101
+ if (cached) {
102
+ _addSymbol(id, cached);
103
+ loaded.add(href);
104
+ return;
105
+ }
106
+ } catch (e) { /* proceed to fetch */ }
107
+
108
+ pending.add(href);
109
+
110
+ fetch(_url(href))
111
+ .then(function (r) {
112
+ if (!r.ok) throw new Error(r.status);
113
+ return r.text();
114
+ })
115
+ .then(function (raw) {
116
+ _addSymbol(id, raw);
117
+ loaded.add(href);
118
+ pending.delete(href);
119
+ try { localStorage.setItem(CACHE_PREFIX + id, raw); } catch (e) { /* storage full */ }
120
+ })
121
+ .catch(function () {
122
+ pending.delete(href);
123
+ });
124
+ }
125
+
126
+ function _scan(root) {
127
+ const sel = 'use[href^="' + PREFIX_LN + '"], use[href^="' + PREFIX_LNC + '"]';
128
+ const uses = root.querySelectorAll ? root.querySelectorAll(sel) : [];
129
+ if (root.matches && root.matches(sel)) {
130
+ const h = root.getAttribute('href');
131
+ if (h) _load(h);
132
+ }
133
+ Array.prototype.forEach.call(uses, function (use) {
134
+ const h = use.getAttribute('href');
135
+ if (h) _load(h);
136
+ });
137
+ }
138
+
139
+ function _init() {
140
+ _scan(document);
141
+ new MutationObserver(function (mutations) {
142
+ mutations.forEach(function (m) {
143
+ if (m.type === 'childList') {
144
+ m.addedNodes.forEach(function (node) {
145
+ if (node.nodeType === 1) _scan(node);
146
+ });
147
+ } else if (m.type === 'attributes' && m.attributeName === 'href') {
148
+ // A <use href="..."> was swapped at runtime (e.g. ln-confirm
149
+ // replacing an icon). Trigger a load for the new target.
150
+ const h = m.target.getAttribute('href');
151
+ if (h && (h.indexOf(PREFIX_LN) === 0 || h.indexOf(PREFIX_LNC) === 0)) {
152
+ _load(h);
153
+ }
154
+ }
155
+ });
156
+ }).observe(document.body, {
157
+ childList: true,
158
+ subtree: true,
159
+ attributes: true,
160
+ attributeFilter: ['href']
161
+ });
162
+ }
163
+
164
+ if (document.readyState === 'loading') {
165
+ document.addEventListener('DOMContentLoaded', _init);
166
+ } else {
167
+ _init();
168
+ }
169
+ })();
@@ -0,0 +1,303 @@
1
+ # ln-link
2
+
3
+ > Turns a container into a click-navigable surface by triggering
4
+ > `.click()` on the first inner `<a>`. Hover surfaces the URL in a
5
+ > bottom-left status bar that mirrors the browser's native preview.
6
+
7
+ ## Philosophy
8
+
9
+ ### The first `<a>` rule
10
+
11
+ The contract is `row.querySelector('a')` — the first `<a>` in document
12
+ order. A table row whose primary action is "View" puts the View link
13
+ in cell 1. The component does not consult a `data-primary` attribute
14
+ or any other hint; position IS the contract.
15
+
16
+ ### Augmentation, not replacement
17
+
18
+ The component does not intercept the platform's link click. When the
19
+ row chrome is clicked, the component resolves the first `<a>` and
20
+ calls `link.click()` — the platform handles everything from there:
21
+ `target="_blank"`, `download`, `[data-ln-ajax]` body delegation,
22
+ browser extensions. Composing with other components costs nothing.
23
+
24
+ ### Interactive children stay interactive
25
+
26
+ Buttons, inputs, selects, textareas, and nested anchors inside a
27
+ row are skipped — clicks originating from those elements never
28
+ trigger row navigation. A row can carry a Delete button, a
29
+ dropdown, and a checkbox alongside the record link without
30
+ collision.
31
+
32
+ ### Three modes, one component
33
+
34
+ The container's `tagName` decides how rows are found:
35
+
36
+ - **`<table>`** — the component finds the `<tbody>` (or falls back
37
+ to the table itself if no `<tbody>` exists), iterates each `<tr>`,
38
+ wires each row.
39
+ - **`<tbody>`** — same row iteration; you placed the attribute one
40
+ level lower.
41
+ - **Anything else** (`<li>`, `<article>`, `<tr>`, `<div>`) — the
42
+ element itself is the single clickable row.
43
+
44
+ ## HTML contract
45
+
46
+ ### Table — all tbody rows become clickable
47
+
48
+ ```html
49
+ <table data-ln-link>
50
+ <thead>
51
+ <tr>
52
+ <th>Name</th>
53
+ <th>Email</th>
54
+ <th>Status</th>
55
+ <th>Actions</th>
56
+ </tr>
57
+ </thead>
58
+ <tbody>
59
+ <tr>
60
+ <td><a href="/users/1">Marko Petrovski</a></td>
61
+ <td>marko@example.com</td>
62
+ <td><span class="badge success">Active</span></td>
63
+ <td>
64
+ <!-- This button is skipped — clicking it does NOT navigate -->
65
+ <button>Delete</button>
66
+ </td>
67
+ </tr>
68
+ <tr>
69
+ <td><a href="/users/2">Ana Stojanova</a></td>
70
+ <td>ana@example.com</td>
71
+ <td><span class="badge error">Inactive</span></td>
72
+ <td>
73
+ <button>Delete</button>
74
+ </td>
75
+ </tr>
76
+ </tbody>
77
+ </table>
78
+ ```
79
+
80
+ Apply `link-row` mixin to the rows for `cursor: pointer` and
81
+ `user-select: none`:
82
+
83
+ ```scss
84
+ // project SCSS
85
+ #users-table tr { @include link-row; }
86
+ ```
87
+
88
+ ### List of cards
89
+
90
+ ```html
91
+ <ul data-ln-link>
92
+ <li>
93
+ <a href="/projects/1"><h3>Project Alpha</h3></a>
94
+ <p>Launched 2025. Click anywhere on this card to open.</p>
95
+ </li>
96
+ <li>
97
+ <a href="/projects/2"><h3>Project Beta</h3></a>
98
+ <p>In progress. Buttons inside cards are still independently clickable.</p>
99
+ <button>Archive</button>
100
+ </li>
101
+ </ul>
102
+ ```
103
+
104
+ In `<ul>` mode, each `<li>` is a row. The component iterates them
105
+ exactly as it iterates `<tr>` elements in table mode.
106
+
107
+ ### Generic article
108
+
109
+ ```html
110
+ <article data-ln-link>
111
+ <h4><a href="/posts/42">Dashboard overview</a></h4>
112
+ <p>The entire article is clickable — not just the link text.</p>
113
+ </article>
114
+ ```
115
+
116
+ In generic mode the `<article>` itself is the row. No children are
117
+ iterated; a single set of listeners attaches to the element.
118
+
119
+ ### Direct `<tr>` — one row only
120
+
121
+ ```html
122
+ <table>
123
+ <tbody>
124
+ <tr data-ln-link>
125
+ <!-- Only this row is navigable; the others are plain rows -->
126
+ <td><a href="/users/99">Special row</a></td>
127
+ </tr>
128
+ <tr>
129
+ <td>Plain row — not navigable</td>
130
+ </tr>
131
+ </tbody>
132
+ </table>
133
+ ```
134
+
135
+ Placing `data-ln-link` on a `<tr>` directly puts it in generic mode
136
+ (the `TR` branch in `_initContainer`) so the row itself becomes the
137
+ single clickable element. Useful when only a subset of rows should be
138
+ navigable.
139
+
140
+ ## Attributes
141
+
142
+ | Attribute | On | Description |
143
+ |---|---|---|
144
+ | `data-ln-link` | `<table>`, `<tbody>`, `<tr>`, or any element | Activates clickable-row behavior. On `<table>` or `<tbody>`, every `<tr>` in the `<tbody>` becomes individually navigable. On any other element, the element itself is the row. |
145
+
146
+ ## Events
147
+
148
+ | Event | Bubbles | Cancelable | `detail` | Dispatched on |
149
+ |---|---|---|---|---|
150
+ | `ln-link:navigate` | yes | yes | `{ target, href, link }` | The row element |
151
+
152
+ The event is dispatched BEFORE `link.click()` is called.
153
+ `detail.target` is the row element, `detail.href` is the resolved
154
+ href string, `detail.link` is the `<a>` element. Calling
155
+ `e.preventDefault()` stops navigation entirely.
156
+
157
+ The event does NOT fire on the modifier-key path (Ctrl+click,
158
+ Cmd+click, middle-click). That path calls `window.open(href, '_blank')`
159
+ directly and bypasses the event dispatch.
160
+
161
+ ```js
162
+ // Log navigation
163
+ document.querySelector('table[data-ln-link]').addEventListener('ln-link:navigate', function (e) {
164
+ analytics.track('row_click', { href: e.detail.href });
165
+ // Don't call e.preventDefault() — navigation continues normally.
166
+ });
167
+
168
+ // Conditional cancel — e.g. prevent navigation while a form is dirty
169
+ document.addEventListener('ln-link:navigate', function (e) {
170
+ if (formIsDirty) {
171
+ e.preventDefault();
172
+ showUnsavedWarning(e.detail.href);
173
+ }
174
+ });
175
+ ```
176
+
177
+ ## JS API
178
+
179
+ ```js
180
+ // Re-scan a root for [data-ln-link] containers and initialize them.
181
+ // Called automatically on DOMContentLoaded and by the MutationObserver.
182
+ // Call manually only for Shadow DOM roots the observer cannot see.
183
+ window.lnLink.init(root);
184
+
185
+ // Clean up listeners on a single container.
186
+ // Operates on the PASSED container only — does NOT traverse descendants.
187
+ // To destroy multiple containers, call destroy on each separately.
188
+ window.lnLink.destroy(container);
189
+ ```
190
+
191
+ `init` traverses descendants; `destroy` does not. See
192
+ [`docs/js/link.md`](../../docs/js/link.md) for the full mechanics
193
+ behind this asymmetry.
194
+
195
+ ## What it does NOT do
196
+
197
+ - **Does not intercept clicks on the `<a>` itself.** Direct anchor
198
+ clicks follow the platform default. The row click is an additional
199
+ surface, not a replacement.
200
+ - **Does not fire `ln-link:navigate` on the modifier-key path.**
201
+ Ctrl/Cmd+click and middle-click call `window.open` directly; the
202
+ event is not dispatched and cannot be cancelled.
203
+ - **Does not re-check anchor-less rows on subsequent `init()`
204
+ calls.** A row without an `<a>` at init time still receives the
205
+ per-row guard flag, so re-init is a no-op. To wire a row after
206
+ adding its anchor dynamically, call `destroy(container)` then
207
+ `init(container)`.
208
+ - **Does not wire keyboard navigation.** The row element is not
209
+ made focusable. Keyboard users tab to the inner `<a>` and
210
+ activate it normally.
211
+ - **Does not own appearance.** `cursor: pointer` and
212
+ `user-select: none` come from `@mixin link-row`
213
+ (`scss/config/mixins/_link.scss`); the status bar styling comes
214
+ from `@mixin link-status`. JS owns behavior; SCSS owns
215
+ appearance.
216
+
217
+ ## Cross-component composition
218
+
219
+ ### With ln-ajax
220
+
221
+ `ln-link` and `ln-ajax` compose without any wiring. `ln-link`
222
+ triggers `link.click()` on the inner `<a>`; `ln-ajax` listens for
223
+ `click` events at the body via event delegation and intercepts
224
+ qualifying anchor clicks. The `link.click()` call produces a real
225
+ `click` event on a real `<a>`, so `ln-ajax` sees it exactly as if the
226
+ user had clicked the link directly.
227
+
228
+ ```html
229
+ <!-- ln-ajax wraps the table; ln-link is on the table itself -->
230
+ <section data-ln-ajax>
231
+ <table data-ln-link>
232
+ <thead>
233
+ <tr><th>Name</th><th>Email</th></tr>
234
+ </thead>
235
+ <tbody>
236
+ <tr>
237
+ <td><a href="/users/1" data-ln-ajax-target="main">Marko</a></td>
238
+ <td>marko@example.com</td>
239
+ </tr>
240
+ </tbody>
241
+ </table>
242
+ </section>
243
+ ```
244
+
245
+ No special configuration. The `data-ln-ajax-target` attribute on the
246
+ `<a>` is respected because `ln-ajax` is processing the click on the
247
+ real anchor, not on the row.
248
+
249
+ ### With ln-confirm
250
+
251
+ `ln-confirm` attaches to a `<button>` and intercepts that button's
252
+ click; `ln-link` triggers `.click()` on an `<a>`. There is no shared
253
+ click event for `ln-confirm` to intercept.
254
+
255
+ For confirmation flows on row navigation, use `ln-link:navigate`
256
+ with `e.preventDefault()` instead — see §Events.
257
+
258
+ ### With ln-data-table
259
+
260
+ `ln-data-table` inserts `<tr>` elements into a `<tbody>` from a
261
+ template. `ln-link`'s MutationObserver wires each new row as it
262
+ arrives — no manual re-init is needed after `ln-data-table`
263
+ populates the body.
264
+
265
+ ```html
266
+ <table data-ln-link data-ln-table>
267
+ <thead>...</thead>
268
+ <tbody>
269
+ <!-- ln-data-table inserts <tr> elements here dynamically;
270
+ ln-link wires them automatically. -->
271
+ </tbody>
272
+ </table>
273
+ ```
274
+
275
+ ## Integration & Development
276
+
277
+ ### Integration
278
+
279
+ To integrate `ln-link` into your application, you can choose between loading the unified bundle or importing the component standalone.
280
+
281
+ #### In-Bundle (Standard Integration)
282
+ This is the recommended approach for standard integration, loading the component as part of the main `ln-ashlar` bundle:
283
+ ```html
284
+ <script src="dist/ln-ashlar.iife.js" defer></script>
285
+ ```
286
+
287
+ #### Standalone (Zero-Dependency IIFE)
288
+ If you only need this component and want to avoid loading the full bundle, you can load it standalone using its zero-dependency compiled IIFE:
289
+ ```html
290
+ <script src="js/ln-link/ln-link.js" defer></script>
291
+ ```
292
+
293
+ ### Source Files
294
+
295
+ - **Active Development (Source of Truth)**: [js/ln-link/src/ln-link.js](file:///c:/laragon/www/ln-ashlar/js/ln-link/src/ln-link.js)
296
+ - **Compiled Standalone Distribution**: [js/ln-link/ln-link.js](file:///c:/laragon/www/ln-ashlar/js/ln-link/ln-link.js)
297
+
298
+ ## See also
299
+
300
+ - [`../../docs/js/link.md`](../../docs/js/link.md) — architecture mirror (internal state, observer topology, click-flow, registration pattern)
301
+ - [`../../docs/js/ajax.md`](../../docs/js/ajax.md) — ln-ajax interop details
302
+ - [`../../scss/config/mixins/_link.scss`](../../scss/config/mixins/_link.scss) — `link-row` and `link-status` mixins
303
+ - [`../../demo/admin/src/pages/link.html`](../../demo/admin/src/pages/link.html) — interactive demo
@@ -0,0 +1 @@
1
+ (function(){"use strict";function v(r,t,l){const u=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:l||{}});return r.dispatchEvent(u),u}function d(r,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){d(r,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}r()}const a={};function _(r,t){a[r]=t}function E(r){return a[r]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=_,window.lnCore.getDataMapper=E),(function(){const r="data-ln-link",t="lnLink";if(window[t]!==void 0)return;let l=null;function u(){l=document.createElement("div"),l.className="ln-link-status",document.body.appendChild(l)}function m(e){l&&(l.textContent=e,l.classList.add("ln-link-status--visible"))}function p(){l&&l.classList.remove("ln-link-status--visible")}function g(e,n){if(n.target.closest("a, button, input, select, textarea"))return;const i=e.querySelector("a");if(!i)return;const o=i.getAttribute("href");if(!o)return;if(n.ctrlKey||n.metaKey||n.button===1){window.open(o,"_blank");return}v(e,"ln-link:navigate",{target:e,href:o,link:i}).defaultPrevented||i.click()}function C(e){const n=e.querySelector("a");if(!n)return;const i=n.getAttribute("href");i&&m(i)}function f(){p()}function s(e){e[t+"Row"]||(e[t+"Row"]=!0,e.querySelector("a")&&(e._lnLinkClick=function(n){g(e,n)},e._lnLinkEnter=function(){C(e)},e.addEventListener("click",e._lnLinkClick),e.addEventListener("mouseenter",e._lnLinkEnter),e.addEventListener("mouseleave",f)))}function b(e){e[t+"Row"]&&(e._lnLinkClick&&e.removeEventListener("click",e._lnLinkClick),e._lnLinkEnter&&e.removeEventListener("mouseenter",e._lnLinkEnter),e.removeEventListener("mouseleave",f),delete e._lnLinkClick,delete e._lnLinkEnter,delete e[t+"Row"])}function h(e){if(!e[t+"Init"])return;const n=e.tagName;if(n==="TABLE"||n==="TBODY"){const i=n==="TABLE"&&e.querySelector("tbody")||e;for(const o of i.querySelectorAll("tr"))b(o)}else b(e);delete e[t+"Init"]}function L(e){if(e[t+"Init"])return;e[t+"Init"]=!0;const n=e.tagName;if(n==="TABLE"||n==="TBODY"){const i=n==="TABLE"&&e.querySelector("tbody")||e;for(const o of i.querySelectorAll("tr"))s(o)}else s(e)}function c(e){e.hasAttribute&&e.hasAttribute(r)&&L(e);const n=e.querySelectorAll?e.querySelectorAll("["+r+"]"):[];for(const i of n)L(i)}function S(){d(function(){new MutationObserver(function(n){for(const i of n)if(i.type==="childList")for(const o of i.addedNodes)o.nodeType===1&&(c(o),o.tagName==="TR"&&o.closest("["+r+"]")&&s(o));else i.type==="attributes"&&c(i.target)}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[r]})},"ln-link")}function k(e){c(e)}window[t]={init:k,destroy:h};function y(){u(),S(),k(document.body)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",y):y()})()})();