@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.
Files changed (217) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +162 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +77 -41
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +92 -2
  66. package/js/ln-filter/ln-filter-dev.scss +8 -0
  67. package/js/ln-filter/ln-filter.js +1 -1
  68. package/js/ln-filter/src/ln-filter.js +22 -7
  69. package/js/ln-form/README.md +212 -49
  70. package/js/ln-form/ln-form.js +1 -1
  71. package/js/ln-form/src/ln-form.js +99 -199
  72. package/js/ln-http/README.md +125 -89
  73. package/js/ln-http/ln-http.js +1 -1
  74. package/js/ln-http/src/ln-http.js +32 -13
  75. package/js/ln-icon/README.md +130 -0
  76. package/js/ln-icon/ln-icon.js +1 -0
  77. package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
  78. package/js/ln-link/README.md +47 -9
  79. package/js/ln-link/ln-link.js +1 -1
  80. package/js/ln-link/src/ln-link.js +22 -6
  81. package/js/ln-list/README.md +129 -0
  82. package/js/ln-list/ln-list.js +1 -0
  83. package/js/ln-list/ln-list.scss +28 -0
  84. package/js/ln-list/src/ln-list.js +1216 -0
  85. package/js/ln-modal/README.md +185 -154
  86. package/js/ln-modal/ln-modal-dev.scss +13 -0
  87. package/js/ln-modal/ln-modal.js +1 -1
  88. package/js/ln-modal/ln-modal.scss +28 -4
  89. package/js/ln-modal/src/ln-modal.js +137 -201
  90. package/js/ln-modal-coordinator/README.md +99 -0
  91. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
  92. package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
  93. package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
  94. package/js/ln-nav/README.md +46 -3
  95. package/js/ln-nav/ln-nav.js +1 -1
  96. package/js/ln-nav/src/ln-nav.js +92 -127
  97. package/js/ln-number/README.md +28 -1
  98. package/js/ln-number/ln-number-dev.scss +13 -0
  99. package/js/ln-number/ln-number.js +1 -1
  100. package/js/ln-number/src/ln-number.js +443 -350
  101. package/js/ln-options/README.md +66 -0
  102. package/js/ln-options/ln-options.js +1 -0
  103. package/js/ln-options/src/ln-options.js +74 -0
  104. package/js/ln-popover/README.md +40 -5
  105. package/js/ln-popover/ln-popover.js +1 -1
  106. package/js/ln-popover/src/ln-popover.js +38 -11
  107. package/js/ln-progress/README.md +413 -442
  108. package/js/ln-progress/ln-progress.js +1 -1
  109. package/js/ln-progress/src/ln-progress.js +98 -150
  110. package/js/ln-router/README.md +432 -0
  111. package/js/ln-router/ln-router.js +1 -0
  112. package/js/ln-router/src/ln-router.js +539 -0
  113. package/js/ln-search/README.md +158 -83
  114. package/js/ln-search/ln-search-dev.scss +8 -0
  115. package/js/ln-search/ln-search.js +1 -1
  116. package/js/ln-search/src/ln-search.js +12 -9
  117. package/js/ln-slug/README.md +110 -0
  118. package/js/ln-slug/ln-slug.js +1 -0
  119. package/js/ln-slug/src/ln-slug.js +74 -0
  120. package/js/ln-sort/README.md +170 -0
  121. package/js/ln-sort/ln-sort-dev.scss +8 -0
  122. package/js/ln-sort/ln-sort.js +1 -0
  123. package/js/ln-sort/src/ln-sort.js +178 -0
  124. package/js/ln-sortable/README.md +133 -95
  125. package/js/ln-sortable/ln-sortable.js +1 -1
  126. package/js/ln-stat/README.md +55 -0
  127. package/js/ln-stat/ln-stat.js +1 -0
  128. package/js/ln-stat/src/ln-stat.js +55 -0
  129. package/js/ln-table/README.md +285 -101
  130. package/js/ln-table/ln-table-dev.scss +17 -0
  131. package/js/ln-table/ln-table.js +1 -1
  132. package/js/ln-table/ln-table.scss +1 -1
  133. package/js/ln-table/src/ln-table.js +1493 -473
  134. package/js/ln-table-coordinator/README.md +73 -0
  135. package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
  136. package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
  137. package/js/ln-tabs/README.md +221 -137
  138. package/js/ln-tabs/ln-tabs-dev.scss +18 -0
  139. package/js/ln-tabs/ln-tabs.js +1 -1
  140. package/js/ln-tabs/src/ln-tabs.js +41 -24
  141. package/js/ln-time/README.md +14 -0
  142. package/js/ln-time/ln-time.js +1 -1
  143. package/js/ln-time/src/ln-time.js +223 -192
  144. package/js/ln-toast/README.md +178 -122
  145. package/js/ln-toast/ln-toast.js +1 -15
  146. package/js/ln-toast/src/ln-toast.js +232 -210
  147. package/js/ln-toast/template.html +16 -12
  148. package/js/ln-toggle/README.md +61 -2
  149. package/js/ln-toggle/ln-toggle-dev.scss +13 -0
  150. package/js/ln-toggle/ln-toggle.js +1 -1
  151. package/js/ln-toggle/src/ln-toggle.js +56 -43
  152. package/js/ln-tooltip/README.md +95 -58
  153. package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
  154. package/js/ln-tooltip/ln-tooltip.js +1 -1
  155. package/js/ln-tooltip/src/ln-tooltip.js +45 -6
  156. package/js/ln-translations/README.md +118 -96
  157. package/js/ln-translations/ln-translations.js +1 -1
  158. package/js/ln-translations/src/ln-translations.js +10 -5
  159. package/js/ln-upload/README.md +85 -10
  160. package/js/ln-upload/ln-upload.js +1 -1
  161. package/js/ln-upload/src/ln-upload.js +26 -10
  162. package/js/ln-validate/README.md +72 -0
  163. package/js/ln-validate/ln-validate-dev.scss +15 -0
  164. package/js/ln-validate/ln-validate.js +1 -1
  165. package/js/ln-validate/src/ln-validate.js +64 -0
  166. package/package.json +61 -56
  167. package/scss/base/_global.scss +2 -2
  168. package/scss/base/_typography.scss +1 -2
  169. package/scss/components/_date.scss +1 -0
  170. package/scss/components/_dropdown.scss +0 -5
  171. package/scss/components/_editor.scss +5 -0
  172. package/scss/components/_filter.scss +26 -25
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +13 -3
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +36 -23
  177. package/scss/components/_toast.scss +68 -41
  178. package/scss/components/_toggle.scss +16 -0
  179. package/scss/config/_density.scss +205 -117
  180. package/scss/config/_icons.scss +10 -9
  181. package/scss/config/_tokens.scss +69 -47
  182. package/scss/config/mixins/_accordion.scss +2 -7
  183. package/scss/config/mixins/_alert.scss +4 -4
  184. package/scss/config/mixins/_breadcrumbs.scss +2 -2
  185. package/scss/config/mixins/_btn.scss +1 -1
  186. package/scss/config/mixins/_card.scss +89 -14
  187. package/scss/config/mixins/_collapsible.scss +3 -0
  188. package/scss/config/mixins/_colors.scss +1 -1
  189. package/scss/config/mixins/_diagnostics.scss +11 -0
  190. package/scss/config/mixins/_display.scss +1 -0
  191. package/scss/config/mixins/_dropdown.scss +3 -5
  192. package/scss/config/mixins/_editor.scss +124 -0
  193. package/scss/config/mixins/_empty-state.scss +7 -0
  194. package/scss/config/mixins/_filter.scss +3 -5
  195. package/scss/config/mixins/_footer.scss +1 -2
  196. package/scss/config/mixins/_form.scss +207 -21
  197. package/scss/config/mixins/_index.scss +2 -0
  198. package/scss/config/mixins/_ln-table.scss +41 -2
  199. package/scss/config/mixins/_modal.scss +149 -66
  200. package/scss/config/mixins/_nav.scss +3 -4
  201. package/scss/config/mixins/_popover.scss +39 -0
  202. package/scss/config/mixins/_sidebar.scss +1 -1
  203. package/scss/config/mixins/_sort.scss +42 -0
  204. package/scss/config/mixins/_table.scss +85 -60
  205. package/scss/config/mixins/_toast.scss +179 -152
  206. package/scss/config/mixins/_tooltip.scss +17 -7
  207. package/scss/config/mixins/_typography.scss +4 -2
  208. package/scss/ln-ashlar-dev.scss +27 -9
  209. package/scss/ln-ashlar.scss +7 -1
  210. package/js/ln-data-table/README.md +0 -110
  211. package/js/ln-data-table/ln-data-table.js +0 -1
  212. package/js/ln-data-table/ln-data-table.scss +0 -10
  213. package/js/ln-data-table/src/ln-data-table.js +0 -1087
  214. package/js/ln-icons/README.md +0 -88
  215. package/js/ln-icons/ln-icons.js +0 -1
  216. package/js/ln-table/ln-table-sort.js +0 -1
  217. package/js/ln-table/src/ln-table-sort.js +0 -156
@@ -0,0 +1,66 @@
1
+ # ln-options
2
+
3
+ Populates a `<select>` element with records from an `ln-data-store` via the `ln-data-coordinator` binder. Opt-in: add `data-ln-options="<storeName>"` to any `<select>` inside (or reachable by) a coordinator subtree.
4
+
5
+ No `<template>` needed — ln-options writes `<option>` elements directly (value + label only).
6
+
7
+ ---
8
+
9
+ ## Attributes
10
+
11
+ | Attribute | Element | Description |
12
+ |---|---|---|
13
+ | `data-ln-options="<storeName>"` | `<select>` | Store name to bind. Must match the coordinator's child store name. |
14
+ | `data-ln-options-value="<field>"` | `<select>` | Record field used as `<option value>`. Default: `id`. |
15
+ | `data-ln-options-label="<field>"` | `<select>` | Record field used as `<option>` text. Default: `name`. May be a presenter-computed field. |
16
+
17
+ ---
18
+
19
+ ## Events
20
+
21
+ ### Dispatched
22
+ - `ln-options:request-data` `{ options: storeName }` — fired on init (bubbles to coordinator).
23
+
24
+ ### Received
25
+ - `ln-options:set-data` `{ data }` — coordinator delivers this; component rebuilds options and restores previous selection.
26
+
27
+ ---
28
+
29
+ ## Example
30
+
31
+ ```html
32
+ <div data-ln-data-coordinator>
33
+ <div data-ln-data-store="tenants"></div>
34
+ <div data-ln-api-connector data-ln-api-endpoint="/api/tenants"></div>
35
+
36
+ <select data-ln-options="tenants"
37
+ data-ln-options-value="id"
38
+ data-ln-options-label="name">
39
+ <option value="">All tenants</option>
40
+ </select>
41
+ </div>
42
+ ```
43
+
44
+ The placeholder `<option value="">` is preserved across refreshes. The previous selection is restored if it still exists in the refreshed data.
45
+
46
+ ---
47
+
48
+ ## Notes
49
+
50
+ - ln-options never imports or reads `ln-data-store` directly — it is a pure event client.
51
+ - The coordinator re-serves `ln-options:set-data` on every store mutation (`ln-data-store:ready`, `created`, `updated`, `deleted`, `synced`).
52
+ - `data-ln-options-label` may reference a presenter-computed field (e.g. `full_name`) because the coordinator delivers already-decorated records from `store.getAll({})`.
53
+
54
+ ---
55
+
56
+ ## 🔧 Internals
57
+
58
+ Source: `js/ln-options/src/ln-options.js`. Pure event client — never imports or reads `ln-data-store` directly.
59
+
60
+ ### Coordinator wiring
61
+
62
+ The coordinator's `_serveOptions` handles `ln-options:request-data`: reads `data-ln-options` off the dispatching element, guards with `_ownsStore(name)`, calls `store.getAll({})` (no sort/filter/pagination — options always show the full set), and dispatches `ln-options:set-data { data }` back on the element. On every store mutation (`ready`, `created`, `updated`, `deleted`, `synced`), the coordinator's `_refreshAll()` re-serves every bound `<select>`.
63
+
64
+ ### `<option>` creation
65
+
66
+ The one sanctioned `document.createElement` case in the library (mirrors `ln-table`'s select-all checkbox) — a native `<option>` carries only `value` + text, so a `<template>` would add ceremony for two string assignments.
@@ -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 g={};function T(t,e){g[t]||(g[t]=document.querySelector('[data-ln-template="'+t+'"]'));const n=g[t];return n?n.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function q(t,e,n){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:n||{}}))}function A(t,e){if(!t||!e)return t;const n=t.querySelectorAll("[data-ln-field]");for(let o=0;o<n.length;o++){const i=n[o],l=i.getAttribute("data-ln-field");e[l]!=null&&(i.textContent=e[l])}const c=t.querySelectorAll("[data-ln-attr]");for(let o=0;o<c.length;o++){const i=c[o],l=i.getAttribute("data-ln-attr").split(",");for(let d=0;d<l.length;d++){const r=l[d].trim().split(":");if(r.length!==2)continue;const u=r[0].trim(),f=r[1].trim();e[f]!=null&&i.setAttribute(u,e[f])}}const a=t.querySelectorAll("[data-ln-show]");for(let o=0;o<a.length;o++){const i=a[o],l=i.getAttribute("data-ln-show");l in e&&i.classList.toggle("hidden",!e[l])}const s=t.querySelectorAll("[data-ln-class]");for(let o=0;o<s.length;o++){const i=s[o],l=i.getAttribute("data-ln-class").split(",");for(let d=0;d<l.length;d++){const r=l[d].trim().split(":");if(r.length!==2)continue;const u=r[0].trim(),f=r[1].trim();f in e&&i.classList.toggle(u,!!e[f])}}return t}function F(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const n=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let c=0;c<n.length;c++)n[c].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 n=0;n<e.length;n++)e[n].textContent=""}})));function x(t,e){if(!t||!e)return t;const n=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;n.nextNode();){const s=n.currentNode;s.textContent.indexOf("{{")!==-1&&(s.textContent=s.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(o,i){return e[i]!==void 0?e[i]:""}))}const c=function(s,o){return e[o]!==void 0?e[o]:""},a=Array.from(t.querySelectorAll("*"));t.nodeType===1&&a.push(t);for(let s=0;s<a.length;s++){const o=a[s],i=o.attributes;for(let l=0;l<i.length;l++){const d=i[l];d.value.indexOf("{{")!==-1&&o.setAttribute(d.name,d.value.replace(/\{\{\s*(\w+)\s*\}\}/g,c))}}return t}function N(t,e,n,c,a,s){const o={};for(let l=0;l<t.children.length;l++){const d=t.children[l],r=d.getAttribute("data-ln-key");r&&(o[r]=d)}const i=document.createDocumentFragment();for(let l=0;l<e.length;l++){const d=e[l],r=String(c(d));let u=o[r];if(u)a(u,d,l);else{const f=T(n,s);if(!f||(x(f,d),u=f.firstElementChild,!u))continue;u.setAttribute("data-ln-key",r),a(u,d,l)}i.appendChild(u)}t.textContent="",t.appendChild(i)}function L(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){L(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function w(t,e,n,c){if(t.nodeType!==1)return;const s=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",o=Array.from(t.querySelectorAll(s));t.matches&&t.matches(s)&&o.push(t);for(const i of o)i[n]||(i[n]=new c(i))}function M(t,e,n,c,a={}){const s=a.extraAttributes||[],o=a.onAttributeChange||null,i=a.onInit||null;function l(d){const r=d||document.body;w(r,t,e,n),i&&i(r)}return L(function(){const d=new MutationObserver(function(u){for(let f=0;f<u.length;f++){const h=u[f];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const p=h.addedNodes[m];p.nodeType===1&&(w(p,t,e,n),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+"]",y=Array.from(p.querySelectorAll(k));p.matches&&p.matches(k)&&y.push(p);for(let C=0;C<y.length;C++){const D=y[C];if(!document.contains(D)){const v=D[e];v&&typeof v.destroy=="function"&&v.destroy()}}}}}else h.type==="attributes"&&(o&&h.target[e]?o(h.target,h.attributeName):(w(h.target,t,e,n),i&&i(h.target)))}});let r=[];if(t.indexOf("[")!==-1){const u=/\[([\w-]+)/g;let f;for(;(f=u.exec(t))!==null;)r.push(f[1])}else r.push(t);d.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:r.concat(s)})},c),window[e]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const S={};function j(t,e){S[t]=e}function W(t){return S[t]||{ingress:e=>e,egress:e=>e}}const _={};function E(t,e){if(!t||typeof e!="object")return;const n=t.toLowerCase().split("-")[0];_[n]=e}function B(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return _[e]||null}E("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=j,window.lnCore.getDataMapper=W,window.lnCore.registerLocaleFallback=E,window.lnCore.getLocaleFallback=B,window.lnCore.fillTemplate=x,window.lnCore.fill=A,window.lnCore.lnFill=F,window.lnCore.renderList=N);function O(t){return(t||"").replace(/^#/,"")}function b(t){const e=t===void 0?location.hash:t,n={},c=O(e);if(!c)return n;const a=c.split("&");for(let s=0;s<a.length;s++){const o=a[s];if(!o)continue;const i=o.indexOf(":"),l=i>-1?o.slice(0,i):o,d=i>-1?o.slice(i+1):"";if(l)try{n[l]=decodeURIComponent(d)}catch{n[l]=d}}return n}function I(t){if(!t)return null;const e=b();return t in e?e[t]:null}function K(t,e){if(!t)return;const n=b();e==null?delete n[t]:n[t]=String(e);const a=Object.keys(n).map(function(s){const o=n[s];return o===""?s:s+":"+encodeURIComponent(o)}).join("&");O(location.hash)!==a&&(location.hash=a)}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=b,window.lnCore.hashGet=I,window.lnCore.hashSet=K,window.lnCore.hashLinkClick=R),(function(){const t="data-ln-options",e="lnOptions";if(window[e]!==void 0)return;function n(c){this.dom=c,this._storeName=c.getAttribute(t),this._valueField=c.getAttribute("data-ln-options-value")||"id",this._labelField=c.getAttribute("data-ln-options-label")||"name";const a=this;return this._onSetData=function(s){a._rebuild(s.detail.data||[])},c.addEventListener("ln-options:set-data",this._onSetData),q(c,"ln-options:request-data",{options:this._storeName}),this}n.prototype._rebuild=function(c){const a=this.dom,s=this._valueField,o=this._labelField,i=a.value,l=a.querySelectorAll("option");for(let r=l.length-1;r>=0;r--)l[r].value!==""&&a.removeChild(l[r]);for(let r=0;r<c.length;r++){const u=c[r],f=document.createElement("option");f.value=String(u[s]),f.textContent=u[o]!=null?u[o]:"",a.appendChild(f)}const d=a.options;for(let r=0;r<d.length;r++)if(d[r].value===i){a.value=i;break}},n.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("ln-options:set-data",this._onSetData),delete this.dom[e])},M(t,e,n,"ln-options")})()})();
@@ -0,0 +1,74 @@
1
+ import { registerComponent, dispatch } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-options';
5
+ const DOM_ATTRIBUTE = 'lnOptions';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ // ─── Component ─────────────────────────────────────────────
10
+
11
+ function _component(dom) {
12
+ this.dom = dom;
13
+ this._storeName = dom.getAttribute(DOM_SELECTOR);
14
+ this._valueField = dom.getAttribute('data-ln-options-value') || 'id';
15
+ this._labelField = dom.getAttribute('data-ln-options-label') || 'name';
16
+
17
+ const self = this;
18
+
19
+ this._onSetData = function (e) {
20
+ self._rebuild(e.detail.data || []);
21
+ };
22
+ dom.addEventListener('ln-options:set-data', this._onSetData);
23
+
24
+ // Request initial data from coordinator
25
+ dispatch(dom, 'ln-options:request-data', { options: this._storeName });
26
+
27
+ return this;
28
+ }
29
+
30
+ _component.prototype._rebuild = function (records) {
31
+ const dom = this.dom;
32
+ const valueField = this._valueField;
33
+ const labelField = this._labelField;
34
+
35
+ // Preserve current selection
36
+ const prev = dom.value;
37
+
38
+ // Remove all non-placeholder options (keep option[value=""])
39
+ const existing = dom.querySelectorAll('option');
40
+ for (let i = existing.length - 1; i >= 0; i--) {
41
+ if (existing[i].value !== '') {
42
+ dom.removeChild(existing[i]);
43
+ }
44
+ }
45
+
46
+ // Append one option per record
47
+ for (let i = 0; i < records.length; i++) {
48
+ const rec = records[i];
49
+ const opt = document.createElement('option');
50
+ opt.value = String(rec[valueField]);
51
+ opt.textContent = rec[labelField] != null ? rec[labelField] : '';
52
+ dom.appendChild(opt);
53
+ }
54
+
55
+ // Restore previous selection if still available
56
+ const opts = dom.options;
57
+ for (let i = 0; i < opts.length; i++) {
58
+ if (opts[i].value === prev) {
59
+ dom.value = prev;
60
+ break;
61
+ }
62
+ }
63
+ };
64
+
65
+ _component.prototype.destroy = function () {
66
+ if (!this.dom[DOM_ATTRIBUTE]) return;
67
+ this.dom.removeEventListener('ln-options:set-data', this._onSetData);
68
+ delete this.dom[DOM_ATTRIBUTE];
69
+ };
70
+
71
+ // ─── Init ──────────────────────────────────────────────────
72
+
73
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-options');
74
+ })();
@@ -8,7 +8,7 @@
8
8
 
9
9
  In `ln-ashlar`, the core design principle is **orthogonality**. Rather than creating a heavy component that bundles state, positioning algorithms, LIFO click stacks, and styles, `ln-popover` splits them cleanly:
10
10
 
11
- 1. **State & Positioning (JavaScript)**: The `ln-popover` script handles binary `open` / `closed` states in the DOM, manages view-port boundaries to flip placement as needed, restores keyboard focus gracefully, and coordinates LIFO (Last-In, First-Out) keyboard ESC dismissal stacks.
11
+ 1. **State & Positioning (JavaScript + native Popover API)**: The `ln-popover` script handles binary `open` / `closed` states in the DOM, bridged to the native Popover API (`popover="manual"`, `showPopover()`/`hidePopover()`) purely for top-layer rendering — no ancestor `overflow`/`z-index` can clip or bury it. Because `manual` mode has no native light-dismiss, the script still owns 100% of the behavior: viewport-boundary flip placement, keyboard focus save/restore, and the LIFO (Last-In, First-Out) ESC dismissal stack.
12
12
  2. **Visual Presentation (CSS)**: Visual layouts, background shadows, and borders are handled in Vanilla CSS. The library ships a premium `@mixin popover` to style popover cards.
13
13
  3. **Trigger decoupling (HTML)**: Triggers (`data-ln-popover-for="popoverId"`) and popover containers (`id="popoverId"`) are bound purely by ID, allowing the trigger button and the popup card to live anywhere in the document.
14
14
 
@@ -22,7 +22,7 @@ Triggers and popovers are paired by ID. Wrap content in a `div` carrying the `da
22
22
  <!-- Trigger anywhere -->
23
23
  <button data-ln-popover-for="account-menu">Account</button>
24
24
 
25
- <!-- Popover anywhere — role="dialog" and tabindex="-1" are injected automatically -->
25
+ <!-- Popover anywhere — role="dialog", tabindex="-1", and popover="manual" are injected automatically -->
26
26
  <div data-ln-popover id="account-menu">
27
27
  <p><strong>user@example.com</strong></p>
28
28
  <nav>
@@ -41,7 +41,7 @@ Triggers and popovers are paired by ID. Wrap content in a `div` carrying the `da
41
41
 
42
42
  ## 3. Declarative API & State Contract
43
43
 
44
- There are no imperative JavaScript methods (like `open()` or `close()`) on the component instance. **The HTML attribute is the sole contract.**
44
+ The initialized instance is exposed as `dom.lnPopover`, with `open(trigger)`, `close()`, `toggle(trigger)`, and `destroy()` methods, plus `isOpen` and `trigger` properties. **The HTML attribute remains the primary contract** — the instance methods are thin wrappers that ultimately write the same attribute.
45
45
 
46
46
  Triggers, LIFO click outside handlers, viewport edge flips, and custom scripts all change state by writing the active attribute on the popover element:
47
47
 
@@ -54,6 +54,10 @@ popover.setAttribute('data-ln-popover', 'open');
54
54
  // Close the popover
55
55
  popover.setAttribute('data-ln-popover', 'closed');
56
56
 
57
+ // Equivalent imperative calls via the instance
58
+ popover.lnPopover.open();
59
+ popover.lnPopover.close();
60
+
57
61
  // Read-only state query
58
62
  popover.lnPopover.isOpen; // Returns true/false
59
63
  ```
@@ -122,6 +126,37 @@ Opening B from inside A leaves A open. Pressing `ESC` once closes B first, and t
122
126
  ---
123
127
 
124
128
  ## Related
125
- - **[`ln-dropdown`](../ln-dropdown/README.md)** — Menu wrapper adding click-outside/teleportation.
129
+ - **[`ln-dropdown`](../ln-dropdown/README.md)** — Menu wrapper adding click-outside/top-layer promotion.
126
130
  - **[`ln-toggle`](../ln-toggle/README.md)** — Binary disclosure state primitive.
127
- - **Architecture deep-dive** — [`docs/js/popover.md`](../../docs/js/popover.md).
131
+
132
+ ---
133
+
134
+ ## 🔧 Internals
135
+
136
+ Source: `js/ln-popover/ln-popover.js`.
137
+
138
+ ### Open-stack
139
+
140
+ A module-level `openStack` array tracks all currently-open instances. It drives two behaviors: a single `document` `keydown` listener, registered only while the stack is non-empty, pops the top entry and calls `.close()` on it (LIFO Escape); and nested popovers stay independent because each owns its own outside-click listener scoped to its own subtree.
141
+
142
+ ### `_applyOpen(trigger)` — after the cancelable `before-open` passes
143
+
144
+ Set `isOpen = true`, store `trigger` (`null` if opened via direct attribute mutation) and `document.activeElement` (for focus restore) → `teleportToBody(dom)` (moves the element to end of `<body>`, storing a restore function, so `position: fixed` coordinates are reliable regardless of ancestor `transform`/`contain`) → `measureHidden(dom)` for dimensions even if `display: none` → `computePlacement(triggerRect, size, preferred, 8)` writes `top`/`left` inline (unavoidable for floating UI) and `data-ln-popover-placement` → `aria-expanded="true"` on trigger → focus the first focusable child, or the container itself → outside-click listener registered one tick later via `setTimeout` (so the opening click itself doesn't immediately close it) → scroll/resize listeners for repositioning while open → push onto `openStack`, ensure ESC listener → dispatch `ln-popover:open`.
145
+
146
+ ### `_applyClose()` — mirror of `_applyOpen`
147
+
148
+ Clears `isOpen`, removes the outside-click/scroll/resize listeners, clears inline `top`/`left` and the placement attribute, sets `aria-expanded="false"`, runs the teleport-restore function, splices itself from `openStack` (removing the ESC listener if the stack is now empty), then restores focus to the trigger **only if** the trigger was the previously-focused element (click→Escape, click→programmatic close) or `activeElement === document.body` (Escape from inside the popover, outside-click on inert whitespace) — an outside-click landing on another focusable element keeps that element's focus. Dispatches `ln-popover:close`.
149
+
150
+ ### Focus management
151
+
152
+ A disclosure pattern, not a modal — no Tab trap. Because the popover is teleported to the end of `<body>`, Tab exits it into whatever follows in body order. `tabindex="-1"` and `role="dialog"` are set automatically at construction so the container itself is a valid focus target when there's no focusable child.
153
+
154
+ ### Nested popovers
155
+
156
+ Outside-click tests `self.dom.contains(e.target)`, so a click inside A's subtree (including a button that opens B) never triggers A's close path. Both popovers land on `openStack`; LIFO Escape closes B first, then A on the next press. There is no "close all."
157
+
158
+ ### Observer & registration
159
+
160
+ Single observer on `document.body` (`subtree`/`childList`/`attributes`, filtered to `data-ln-popover`/`data-ln-popover-for`): childList additions re-run `_findPopovers`/`_attachTriggers`; attribute mutations dispatch to `_syncAttribute` for existing instances or re-run discovery for new ones. Two separate `registerComponent` calls wire popover elements (`el.lnPopover`) and trigger elements (`el.lnPopoverTrigger`) independently — trigger init sets `aria-haspopup="dialog"`, `aria-expanded="false"`, `aria-controls`, once, on attach.
161
+
162
+ `destroy()` on a popover instance closes it if open, removes its listeners, splices it from `openStack`, and dispatches `ln-popover:destroyed`. Trigger `destroy` is independent and only removes the trigger's click handler.
@@ -1 +1 @@
1
- (function(){"use strict";function A(t,e,r){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:r||{}}))}function E(t,e,r){const g=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:r||{}});return t.dispatchEvent(g),g}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 O(t,e,r,g){if(t.nodeType!==1)return;const f=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",m=Array.from(t.querySelectorAll(f));t.matches&&t.matches(f)&&m.push(t);for(const d of m)d[r]||(d[r]=new g(d))}function I(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function k(t,e,r,g,u={}){const f=u.extraAttributes||[],m=u.onAttributeChange||null,d=u.onInit||null;function l(y){const i=y||document.body;O(i,t,e,r),d&&d(i)}return x(function(){const y=new MutationObserver(function(s){for(let a=0;a<s.length;a++){const n=s[a];if(n.type==="childList"){for(let o=0;o<n.addedNodes.length;o++){const p=n.addedNodes[o];p.nodeType===1&&(O(p,t,e,r),d&&d(p))}for(let o=0;o<n.removedNodes.length;o++){const p=n.removedNodes[o];if(p.nodeType===1){const h=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",b=Array.from(p.querySelectorAll(h));p.matches&&p.matches(h)&&b.push(p);for(let c=0;c<b.length;c++){const v=b[c];if(!document.contains(v)){const C=v[e];C&&typeof C.destroy=="function"&&C.destroy()}}}}}else n.type==="attributes"&&(m&&n.target[e]?m(n.target,n.attributeName):(O(n.target,t,e,r),d&&d(n.target)))}});let i=[];if(t.indexOf("[")!==-1){const s=/\[([\w-]+)/g;let a;for(;(a=s.exec(t))!==null;)i.push(a[1])}else i.push(t);y.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:i.concat(f)})},g||(t.indexOf("[")===-1?t.replace("data-",""):"component")),window[e]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const L={};function R(t,e){L[t]=e}function N(t){return L[t]||{ingress:e=>e,egress:e=>e}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=R,window.lnCore.getDataMapper=N);function T(t,e,r,g){const u=g,f=window.innerWidth,m=window.innerHeight,d=e.width,l=e.height,y=(r||"bottom").split("-"),i=y[0],s=y[1]==="start"||y[1]==="end"?y[1]:"center",a={top:["top","bottom","right","left"],bottom:["bottom","top","right","left"],left:["left","right","top","bottom"],right:["right","left","top","bottom"]},n=a[i]||a.bottom;function o(c){return c==="top"||c==="bottom"?s==="start"?t.left:s==="end"?t.right-d:t.left+(t.width-d)/2:s==="start"?t.top:s==="end"?t.bottom-l:t.top+(t.height-l)/2}function p(c){let v,C,w=!0;return c==="top"?(v=t.top-u-l,C=o(c),v<0&&(w=!1)):c==="bottom"?(v=t.bottom+u,C=o(c),v+l>m&&(w=!1)):c==="left"?(v=o(c),C=t.left-u-d,C<0&&(w=!1)):(v=o(c),C=t.right+u,C+d>f&&(w=!1)),{top:v,left:C,side:c,fits:w}}let _=null;for(let c=0;c<n.length;c++){const v=p(n[c]);if(v.fits){_=v;break}}_||(_=p(n[0]));let h=_.top,b=_.left;return d>=f?b=0:(b<0&&(b=0),b+d>f&&(b=f-d)),l>=m?h=0:(h<0&&(h=0),h+l>m&&(h=m-l)),{top:h,left:b,placement:_.side}}function M(t){if(!t||t.parentNode===document.body)return function(){};const e=t.parentNode,r=document.createComment("ln-teleport");return e.insertBefore(r,t),document.body.appendChild(t),function(){r.parentNode&&(r.parentNode.insertBefore(t,r),r.parentNode.removeChild(r))}}function D(t){if(!t)return{width:0,height:0};const e=t.style,r=e.visibility,g=e.display,u=e.position;e.visibility="hidden",e.display="block",e.position="fixed";const f=t.offsetWidth,m=t.offsetHeight;return e.visibility=r,e.display=g,e.position=u,{width:f,height:m}}(function(){const t="data-ln-popover",e="lnPopover",r="data-ln-popover-for",g="data-ln-popover-position";if(window[e]!==void 0)return;const u=[];let f=null;function m(){f||(f=function(i){if(i.key!=="Escape"||u.length===0)return;u[u.length-1].close()},document.addEventListener("keydown",f))}function d(){u.length>0||f&&(document.removeEventListener("keydown",f),f=null)}function l(i){return this.dom=i,this.isOpen=i.getAttribute(t)==="open",this.trigger=null,this._teleportRestore=null,this._previousFocus=null,this._boundDocClick=null,this._docClickTimeout=null,this._boundReposition=null,i.hasAttribute("tabindex")||i.setAttribute("tabindex","-1"),i.hasAttribute("role")||i.setAttribute("role","dialog"),this.isOpen&&this._applyOpen(null),this}l.prototype.open=function(i){this.isOpen||(this.trigger=i||null,this.dom.setAttribute(t,"open"))},l.prototype.close=function(){this.isOpen&&this.dom.setAttribute(t,"closed")},l.prototype.toggle=function(i){this.isOpen?this.close():this.open(i)},l.prototype._applyOpen=function(i){this.isOpen=!0,i&&(this.trigger=i),this._previousFocus=document.activeElement,this._teleportRestore=M(this.dom);const s=D(this.dom);if(this.trigger){const p=this.trigger.getBoundingClientRect(),_=this.dom.getAttribute(g)||"bottom",h=T(p,s,_,8);this.dom.style.top=h.top+"px",this.dom.style.left=h.left+"px",this.dom.setAttribute("data-ln-popover-placement",h.placement),this.trigger.setAttribute("aria-expanded","true")}const a=this.dom.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'),n=Array.prototype.find.call(a,I);n?n.focus():this.dom.focus();const o=this;this._boundDocClick=function(p){o.dom.contains(p.target)||o.trigger&&o.trigger.contains(p.target)||o.close()},o._docClickTimeout=setTimeout(function(){o._docClickTimeout=null,document.addEventListener("click",o._boundDocClick)},0),this._boundReposition=function(){if(!o.trigger)return;const p=o.trigger.getBoundingClientRect(),_=D(o.dom),h=o.dom.getAttribute(g)||"bottom",b=T(p,_,h,8);o.dom.style.top=b.top+"px",o.dom.style.left=b.left+"px",o.dom.setAttribute("data-ln-popover-placement",b.placement)},window.addEventListener("scroll",this._boundReposition,{passive:!0,capture:!0}),window.addEventListener("resize",this._boundReposition),u.push(this),m(),A(this.dom,"ln-popover:open",{popoverId:this.dom.id,target:this.dom,trigger:this.trigger})},l.prototype._applyClose=function(){this.isOpen=!1,this._docClickTimeout&&(clearTimeout(this._docClickTimeout),this._docClickTimeout=null),this._boundDocClick&&(document.removeEventListener("click",this._boundDocClick),this._boundDocClick=null),this._boundReposition&&(window.removeEventListener("scroll",this._boundReposition,{capture:!0}),window.removeEventListener("resize",this._boundReposition),this._boundReposition=null),this.dom.style.top="",this.dom.style.left="",this.dom.removeAttribute("data-ln-popover-placement"),this.trigger&&this.trigger.setAttribute("aria-expanded","false"),this._teleportRestore&&(this._teleportRestore(),this._teleportRestore=null);const i=u.indexOf(this);i!==-1&&u.splice(i,1),d(),this._previousFocus&&this.trigger&&this._previousFocus===this.trigger?this.trigger.focus():this.trigger&&document.activeElement===document.body&&this.trigger.focus(),this._previousFocus=null,A(this.dom,"ln-popover:close",{popoverId:this.dom.id,target:this.dom,trigger:this.trigger}),this.trigger=null},l.prototype.destroy=function(){this.dom[e]&&(this.isOpen&&this._applyClose(),delete this.dom[e],A(this.dom,"ln-popover:destroyed",{popoverId:this.dom.id,target:this.dom}))};function y(i){this.dom=i;const s=i.getAttribute(r);return i.setAttribute("aria-haspopup","dialog"),i.setAttribute("aria-expanded","false"),i.setAttribute("aria-controls",s),this._onClick=function(a){if(a.ctrlKey||a.metaKey||a.button===1)return;a.preventDefault();const n=document.getElementById(s);!n||!n[e]||n[e].toggle(i)},i.addEventListener("click",this._onClick),this}y.prototype.destroy=function(){this.dom.removeEventListener("click",this._onClick),delete this.dom[e+"Trigger"]},k(t,e,l,"ln-popover",{onAttributeChange:function(i){const s=i[e];if(!s)return;const n=i.getAttribute(t)==="open";if(n!==s.isOpen)if(n){if(E(i,"ln-popover:before-open",{popoverId:i.id,target:i,trigger:s.trigger}).defaultPrevented){i.setAttribute(t,"closed");return}s._applyOpen(s.trigger)}else{if(E(i,"ln-popover:before-close",{popoverId:i.id,target:i,trigger:s.trigger}).defaultPrevented){i.setAttribute(t,"open");return}s._applyClose()}}}),k(r,e+"Trigger",y,"ln-popover-trigger")})()})();
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 A={};function N(t,e){A[t]||(A[t]=document.querySelector('[data-ln-template="'+t+'"]'));const s=A[t];return s?s.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function E(t,e,s){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:s||{}}))}function k(t,e,s){const h=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:s||{}});return t.dispatchEvent(h),h}function O(t,e){if(!t||!e)return t;const s=t.querySelectorAll("[data-ln-field]");for(let o=0;o<s.length;o++){const r=s[o],i=r.getAttribute("data-ln-field");e[i]!=null&&(r.textContent=e[i])}const h=t.querySelectorAll("[data-ln-attr]");for(let o=0;o<h.length;o++){const r=h[o],i=r.getAttribute("data-ln-attr").split(",");for(let f=0;f<i.length;f++){const n=i[f].trim().split(":");if(n.length!==2)continue;const u=n[0].trim(),d=n[1].trim();e[d]!=null&&r.setAttribute(u,e[d])}}const c=t.querySelectorAll("[data-ln-show]");for(let o=0;o<c.length;o++){const r=c[o],i=r.getAttribute("data-ln-show");i in e&&r.classList.toggle("hidden",!e[i])}const l=t.querySelectorAll("[data-ln-class]");for(let o=0;o<l.length;o++){const r=l[o],i=r.getAttribute("data-ln-class").split(",");for(let f=0;f<i.length;f++){const n=i[f].trim().split(":");if(n.length!==2)continue;const u=n[0].trim(),d=n[1].trim();d in e&&r.classList.toggle(u,!!e[d])}}return t}function W(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const s=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let h=0;h<s.length;h++)s[h].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)O(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let s=0;s<e.length;s++)e[s].textContent=""}})));function T(t,e){if(!t||!e)return t;const s=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;s.nextNode();){const l=s.currentNode;l.textContent.indexOf("{{")!==-1&&(l.textContent=l.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(o,r){return e[r]!==void 0?e[r]:""}))}const h=function(l,o){return e[o]!==void 0?e[o]:""},c=Array.from(t.querySelectorAll("*"));t.nodeType===1&&c.push(t);for(let l=0;l<c.length;l++){const o=c[l],r=o.attributes;for(let i=0;i<r.length;i++){const f=r[i];f.value.indexOf("{{")!==-1&&o.setAttribute(f.name,f.value.replace(/\{\{\s*(\w+)\s*\}\}/g,h))}}return t}function B(t,e,s,h,c,l){const o={};for(let i=0;i<t.children.length;i++){const f=t.children[i],n=f.getAttribute("data-ln-key");n&&(o[n]=f)}const r=document.createDocumentFragment();for(let i=0;i<e.length;i++){const f=e[i],n=String(h(f));let u=o[n];if(u)c(u,f,i);else{const d=N(s,l);if(!d||(T(d,f),u=d.firstElementChild,!u))continue;u.setAttribute("data-ln-key",n),c(u,f,i)}r.appendChild(u)}t.textContent="",t.appendChild(r)}function q(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){q(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function L(t,e,s,h){if(t.nodeType!==1)return;const l=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",o=Array.from(t.querySelectorAll(l));t.matches&&t.matches(l)&&o.push(t);for(const r of o)r[s]||(r[s]=new h(r))}function j(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function S(t,e,s,h,c={}){const l=c.extraAttributes||[],o=c.onAttributeChange||null,r=c.onInit||null;function i(f){const n=f||document.body;L(n,t,e,s),r&&r(n)}return q(function(){const f=new MutationObserver(function(u){for(let d=0;d<u.length;d++){const p=u[d];if(p.type==="childList"){for(let a=0;a<p.addedNodes.length;a++){const g=p.addedNodes[a];g.nodeType===1&&(L(g,t,e,s),r&&r(g))}for(let a=0;a<p.removedNodes.length;a++){const g=p.removedNodes[a];if(g.nodeType===1){const b=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",y=Array.from(g.querySelectorAll(b));g.matches&&g.matches(b)&&y.push(g);for(let m=0;m<y.length;m++){const v=y[m];if(!document.contains(v)){const C=v[e];C&&typeof C.destroy=="function"&&C.destroy()}}}}}else p.type==="attributes"&&(o&&p.target[e]?o(p.target,p.attributeName):(L(p.target,t,e,s),r&&r(p.target)))}});let n=[];if(t.indexOf("[")!==-1){const u=/\[([\w-]+)/g;let d;for(;(d=u.exec(t))!==null;)n.push(d[1])}else n.push(t);f.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:n.concat(l)})},h||(t.indexOf("[")===-1?t.replace("data-",""):"component")),window[e]=i,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){i(document.body)}):i(document.body),i}const D={};function K(t,e){D[t]=e}function H(t){return D[t]||{ingress:e=>e,egress:e=>e}}const R={};function I(t,e){if(!t||typeof e!="object")return;const s=t.toLowerCase().split("-")[0];R[s]=e}function G(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return R[e]||null}I("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=K,window.lnCore.getDataMapper=H,window.lnCore.registerLocaleFallback=I,window.lnCore.getLocaleFallback=G,window.lnCore.fillTemplate=T,window.lnCore.fill=O,window.lnCore.lnFill=W,window.lnCore.renderList=B);function F(t){return(t||"").replace(/^#/,"")}function x(t){const e=t===void 0?location.hash:t,s={},h=F(e);if(!h)return s;const c=h.split("&");for(let l=0;l<c.length;l++){const o=c[l];if(!o)continue;const r=o.indexOf(":"),i=r>-1?o.slice(0,r):o,f=r>-1?o.slice(r+1):"";if(i)try{s[i]=decodeURIComponent(f)}catch{s[i]=f}}return s}function z(t){if(!t)return null;const e=x();return t in e?e[t]:null}function U(t,e){if(!t)return;const s=x();e==null?delete s[t]:s[t]=String(e);const c=Object.keys(s).map(function(l){const o=s[l];return o===""?l:l+":"+encodeURIComponent(o)}).join("&");F(location.hash)!==c&&(location.hash=c)}function V(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=x,window.lnCore.hashGet=z,window.lnCore.hashSet=U,window.lnCore.hashLinkClick=V);function P(t,e,s,h){const c=h,l=window.innerWidth,o=window.innerHeight,r=e.width,i=e.height,f=(s||"bottom").split("-"),n=f[0],u=f[1]==="start"||f[1]==="end"?f[1]:"center",d={top:["top","bottom","right","left"],bottom:["bottom","top","right","left"],left:["left","right","top","bottom"],right:["right","left","top","bottom"]},p=d[n]||d.bottom;function a(m){return m==="top"||m==="bottom"?u==="start"?t.left:u==="end"?t.right-r:t.left+(t.width-r)/2:u==="start"?t.top:u==="end"?t.bottom-i:t.top+(t.height-i)/2}function g(m){let v,C,_=!0;return m==="top"?(v=t.top-c-i,C=a(m),v<0&&(_=!1)):m==="bottom"?(v=t.bottom+c,C=a(m),v+i>o&&(_=!1)):m==="left"?(v=a(m),C=t.left-c-r,C<0&&(_=!1)):(v=a(m),C=t.right+c,C+r>l&&(_=!1)),{top:v,left:C,side:m,fits:_}}let w=null;for(let m=0;m<p.length;m++){const v=g(p[m]);if(v.fits){w=v;break}}w||(w=g(p[0]));let b=w.top,y=w.left;return r>=l?y=0:(y<0&&(y=0),y+r>l&&(y=l-r)),i>=o?b=0:(b<0&&(b=0),b+i>o&&(b=o-i)),{top:b,left:y,placement:w.side}}function M(t){if(!t)return{width:0,height:0};const e=t.style,s=e.visibility,h=e.display,c=e.position;e.visibility="hidden",e.display="block",e.position="fixed";const l=t.offsetWidth,o=t.offsetHeight;return e.visibility=s,e.display=h,e.position=c,{width:l,height:o}}(function(){const t="data-ln-popover",e="lnPopover",s="data-ln-popover-for",h="data-ln-popover-position";if(window[e]!==void 0)return;const c=[];let l=null;function o(){l||(l=function(n){if(n.key!=="Escape"||c.length===0)return;c[c.length-1].close()},document.addEventListener("keydown",l))}function r(){c.length>0||l&&(document.removeEventListener("keydown",l),l=null)}function i(n){this.dom=n,this.isOpen=n.getAttribute(t)==="open",this.trigger=null,this._previousFocus=null,this._boundDocClick=null,this._docClickTimeout=null,this._boundReposition=null;const u=this;return this._onRequestOpen=function(d){const p=d.detail&&d.detail.trigger?d.detail.trigger:null;u.open(p)},this._onRequestClose=function(){u.close()},this._onRequestToggle=function(d){const p=d.detail&&d.detail.trigger?d.detail.trigger:null;u.toggle(p)},n.addEventListener("ln-popover:request-open",this._onRequestOpen),n.addEventListener("ln-popover:request-close",this._onRequestClose),n.addEventListener("ln-popover:request-toggle",this._onRequestToggle),n.hasAttribute("tabindex")||n.setAttribute("tabindex","-1"),n.hasAttribute("role")||n.setAttribute("role","dialog"),n.hasAttribute("popover")||n.setAttribute("popover","manual"),this.isOpen&&this._applyOpen(null),this}i.prototype.open=function(n){this.isOpen||(this.trigger=n||null,this.dom.setAttribute(t,"open"))},i.prototype.close=function(){this.isOpen&&this.dom.setAttribute(t,"closed")},i.prototype.toggle=function(n){this.isOpen?this.close():this.open(n)},i.prototype._applyOpen=function(n){this.isOpen=!0,n&&(this.trigger=n),this._previousFocus=document.activeElement,typeof this.dom.showPopover=="function"&&this.dom.showPopover();const u=M(this.dom);if(this.trigger){const g=this.trigger.getBoundingClientRect(),w=this.dom.getAttribute(h)||"bottom",b=P(g,u,w,8);this.dom.style.top=b.top+"px",this.dom.style.left=b.left+"px",this.dom.setAttribute("data-ln-popover-placement",b.placement),this.trigger.setAttribute("aria-expanded","true")}const d=this.dom.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'),p=Array.prototype.find.call(d,j);p?p.focus():this.dom.focus();const a=this;this._boundDocClick=function(g){a.dom.contains(g.target)||a.trigger&&a.trigger.contains(g.target)||a.close()},a._docClickTimeout=setTimeout(function(){a._docClickTimeout=null,document.addEventListener("click",a._boundDocClick)},0),this._boundReposition=function(){if(!a.trigger)return;const g=a.trigger.getBoundingClientRect(),w=M(a.dom),b=a.dom.getAttribute(h)||"bottom",y=P(g,w,b,8);a.dom.style.top=y.top+"px",a.dom.style.left=y.left+"px",a.dom.setAttribute("data-ln-popover-placement",y.placement)},window.addEventListener("scroll",this._boundReposition,{passive:!0,capture:!0}),window.addEventListener("resize",this._boundReposition),c.push(this),o(),E(this.dom,"ln-popover:open",{popoverId:this.dom.id,target:this.dom,trigger:this.trigger})},i.prototype._applyClose=function(){this.isOpen=!1,this._docClickTimeout&&(clearTimeout(this._docClickTimeout),this._docClickTimeout=null),this._boundDocClick&&(document.removeEventListener("click",this._boundDocClick),this._boundDocClick=null),this._boundReposition&&(window.removeEventListener("scroll",this._boundReposition,{capture:!0}),window.removeEventListener("resize",this._boundReposition),this._boundReposition=null),this.dom.style.top="",this.dom.style.left="",this.dom.removeAttribute("data-ln-popover-placement"),this.trigger&&this.trigger.setAttribute("aria-expanded","false"),typeof this.dom.hidePopover=="function"&&this.dom.matches(":popover-open")&&this.dom.hidePopover();const n=c.indexOf(this);n!==-1&&c.splice(n,1),r(),this._previousFocus&&this.trigger&&this._previousFocus===this.trigger?this.trigger.focus():this.trigger&&document.activeElement===document.body&&this.trigger.focus(),this._previousFocus=null,E(this.dom,"ln-popover:close",{popoverId:this.dom.id,target:this.dom,trigger:this.trigger}),this.trigger=null},i.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("ln-popover:request-open",this._onRequestOpen),this.dom.removeEventListener("ln-popover:request-close",this._onRequestClose),this.dom.removeEventListener("ln-popover:request-toggle",this._onRequestToggle),this.isOpen&&this._applyClose(),delete this.dom[e],E(this.dom,"ln-popover:destroyed",{popoverId:this.dom.id,target:this.dom}))};function f(n){this.dom=n;const u=n.getAttribute(s);return n.setAttribute("aria-haspopup","dialog"),n.setAttribute("aria-expanded","false"),n.setAttribute("aria-controls",u),this._onClick=function(d){if(d.ctrlKey||d.metaKey||d.button===1)return;d.preventDefault();const p=document.getElementById(u);if(!p)return;p[e]&&(p[e].trigger=n);const a=p.getAttribute(t);p.setAttribute(t,a==="open"?"closed":"open")},n.addEventListener("click",this._onClick),this}f.prototype.destroy=function(){this.dom.removeEventListener("click",this._onClick),delete this.dom[e+"Trigger"]},S(t,e,i,"ln-popover",{onAttributeChange:function(n){const u=n[e];if(!u)return;const p=n.getAttribute(t)==="open";if(p!==u.isOpen)if(p){if(k(n,"ln-popover:before-open",{popoverId:n.id,target:n,trigger:u.trigger}).defaultPrevented){n.setAttribute(t,"closed");return}u._applyOpen(u.trigger)}else{if(k(n,"ln-popover:before-close",{popoverId:n.id,target:n,trigger:u.trigger}).defaultPrevented){n.setAttribute(t,"open");return}u._applyClose()}}}),S(s,e+"Trigger",f,"ln-popover-trigger")})()})();
@@ -1,4 +1,4 @@
1
- import { dispatch, dispatchCancelable, computePlacement, teleportToBody, measureHidden, isVisible, registerComponent } from '../../ln-core';
1
+ import { dispatch, dispatchCancelable, computePlacement, measureHidden, isVisible, registerComponent } from '../../ln-core';
2
2
 
3
3
  (function () {
4
4
  const DOM_SELECTOR = 'data-ln-popover';
@@ -37,12 +37,28 @@ import { dispatch, dispatchCancelable, computePlacement, teleportToBody, measure
37
37
  this.dom = dom;
38
38
  this.isOpen = dom.getAttribute(DOM_SELECTOR) === 'open';
39
39
  this.trigger = null;
40
- this._teleportRestore = null;
41
40
  this._previousFocus = null;
42
41
  this._boundDocClick = null;
43
42
  this._docClickTimeout = null;
44
43
  this._boundReposition = null;
45
44
 
45
+ const self = this;
46
+ this._onRequestOpen = function (e) {
47
+ const trigger = e.detail && e.detail.trigger ? e.detail.trigger : null;
48
+ self.open(trigger);
49
+ };
50
+ this._onRequestClose = function () {
51
+ self.close();
52
+ };
53
+ this._onRequestToggle = function (e) {
54
+ const trigger = e.detail && e.detail.trigger ? e.detail.trigger : null;
55
+ self.toggle(trigger);
56
+ };
57
+
58
+ dom.addEventListener('ln-popover:request-open', this._onRequestOpen);
59
+ dom.addEventListener('ln-popover:request-close', this._onRequestClose);
60
+ dom.addEventListener('ln-popover:request-toggle', this._onRequestToggle);
61
+
46
62
  // Make the popover container itself programmatically focusable
47
63
  // as a fallback when it has no focusable children.
48
64
  if (!dom.hasAttribute('tabindex')) {
@@ -51,6 +67,11 @@ import { dispatch, dispatchCancelable, computePlacement, teleportToBody, measure
51
67
  if (!dom.hasAttribute('role')) {
52
68
  dom.setAttribute('role', 'dialog');
53
69
  }
70
+ // Popover API gives no implicit ARIA role and no focus management —
71
+ // unlike <dialog>, everything above/below this line stays manual.
72
+ if (!dom.hasAttribute('popover')) {
73
+ dom.setAttribute('popover', 'manual');
74
+ }
54
75
 
55
76
  // If the markup says open at boot, sync immediately.
56
77
  if (this.isOpen) {
@@ -86,9 +107,9 @@ import { dispatch, dispatchCancelable, computePlacement, teleportToBody, measure
86
107
  if (trigger) this.trigger = trigger;
87
108
  this._previousFocus = document.activeElement;
88
109
 
89
- // Teleport into <body> so position:fixed coordinates are reliable
90
- // regardless of any ancestor with `transform` or `contain`.
91
- this._teleportRestore = teleportToBody(this.dom);
110
+ // Promote to the top layer so position:fixed coordinates are reliable
111
+ // regardless of any ancestor with `transform`, `contain`, or `overflow: hidden`.
112
+ if (typeof this.dom.showPopover === 'function') this.dom.showPopover();
92
113
 
93
114
  // Measure (works even though it just got `display: block` by the
94
115
  // attribute switch — measureHidden is safe either way).
@@ -180,10 +201,9 @@ import { dispatch, dispatchCancelable, computePlacement, teleportToBody, measure
180
201
  this.trigger.setAttribute('aria-expanded', 'false');
181
202
  }
182
203
 
183
- // Restore teleport.
184
- if (this._teleportRestore) {
185
- this._teleportRestore();
186
- this._teleportRestore = null;
204
+ // Exit the top layer.
205
+ if (typeof this.dom.hidePopover === 'function' && this.dom.matches(':popover-open')) {
206
+ this.dom.hidePopover();
187
207
  }
188
208
 
189
209
  // Remove from open stack.
@@ -211,6 +231,9 @@ import { dispatch, dispatchCancelable, computePlacement, teleportToBody, measure
211
231
 
212
232
  _component.prototype.destroy = function () {
213
233
  if (!this.dom[DOM_ATTRIBUTE]) return;
234
+ this.dom.removeEventListener('ln-popover:request-open', this._onRequestOpen);
235
+ this.dom.removeEventListener('ln-popover:request-close', this._onRequestClose);
236
+ this.dom.removeEventListener('ln-popover:request-toggle', this._onRequestToggle);
214
237
  if (this.isOpen) this._applyClose();
215
238
  delete this.dom[DOM_ATTRIBUTE];
216
239
  dispatch(this.dom, 'ln-popover:destroyed', {
@@ -233,8 +256,12 @@ import { dispatch, dispatchCancelable, computePlacement, teleportToBody, measure
233
256
  if (e.ctrlKey || e.metaKey || e.button === 1) return;
234
257
  e.preventDefault();
235
258
  const target = document.getElementById(popoverId);
236
- if (!target || !target[DOM_ATTRIBUTE]) return;
237
- target[DOM_ATTRIBUTE].toggle(dom);
259
+ if (!target) return;
260
+ if (target[DOM_ATTRIBUTE]) {
261
+ target[DOM_ATTRIBUTE].trigger = dom;
262
+ }
263
+ const current = target.getAttribute(DOM_SELECTOR);
264
+ target.setAttribute(DOM_SELECTOR, current === 'open' ? 'closed' : 'open');
238
265
  };
239
266
  dom.addEventListener('click', this._onClick);
240
267
  return this;