@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,127 @@
1
+ # ln-popover
2
+
3
+ > Viewport-aware click-triggered rich-content overlays, managed reactively via the DOM.
4
+
5
+ ---
6
+
7
+ ## 1. Philosophy & Mindset
8
+
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
+
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.
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
+ 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
+
15
+ ---
16
+
17
+ ## 2. Minimal Blueprint
18
+
19
+ Triggers and popovers are paired by ID. Wrap content in a `div` carrying the `data-ln-popover` attribute.
20
+
21
+ ```html
22
+ <!-- Trigger anywhere -->
23
+ <button data-ln-popover-for="account-menu">Account</button>
24
+
25
+ <!-- Popover anywhere — role="dialog" and tabindex="-1" are injected automatically -->
26
+ <div data-ln-popover id="account-menu">
27
+ <p><strong>user@example.com</strong></p>
28
+ <nav>
29
+ <a href="/settings">Settings</a>
30
+ <a href="/logout">Sign out</a>
31
+ </nav>
32
+ </div>
33
+ ```
34
+
35
+ ### Key Anatomy Rules
36
+ - **The Popover wrapper (`data-ln-popover`)**: Driven by the value `"open"` (open) and `"closed"` or empty (closed).
37
+ - **The Trigger (`data-ln-popover-for="id"`)**: Sets `aria-expanded` and manages focus automatically.
38
+ - **Visual styling**: Re-apply `@mixin popover` in your SCSS on the popover element.
39
+
40
+ ---
41
+
42
+ ## 3. Declarative API & State Contract
43
+
44
+ There are no imperative JavaScript methods (like `open()` or `close()`) on the component instance. **The HTML attribute is the sole contract.**
45
+
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
+
48
+ ```js
49
+ const popover = document.getElementById('account-menu');
50
+
51
+ // Open the popover
52
+ popover.setAttribute('data-ln-popover', 'open');
53
+
54
+ // Close the popover
55
+ popover.setAttribute('data-ln-popover', 'closed');
56
+
57
+ // Read-only state query
58
+ popover.lnPopover.isOpen; // Returns true/false
59
+ ```
60
+
61
+ ### Attributes
62
+ - `data-ln-popover`: Placed on the popover card. Value `"open"` = open; `"closed"` = closed.
63
+ - `data-ln-popover-for="id"`: Placed on triggers referencing the popover ID.
64
+ - `data-ln-popover-position="top|bottom|left|right"`: Preferred placement side. Default: `bottom`.
65
+ - `data-ln-popover-placement`: Set automatically by JS to indicate the *actual* active side after auto-flip is resolved.
66
+
67
+ ---
68
+
69
+ ## 4. Transition Events
70
+
71
+ All events bubble. The dispatch target is the popover element itself.
72
+
73
+ | Event | Cancelable | `detail` | Dispatched When |
74
+ |---|---|---|---|
75
+ | **`ln-popover:before-open`** | **Yes** | `{ popoverId, target, trigger }` | Before opening. `event.preventDefault()` cancels the transition and reverts the attribute. |
76
+ | **`ln-popover:open`** | No | `{ popoverId, target, trigger }` | After popover is positioned, classes added, and focus moved. |
77
+ | **`ln-popover:before-close`** | **Yes** | `{ popoverId, target, trigger }` | Before closing. `event.preventDefault()` cancels the close and reverts the attribute. |
78
+ | **`ln-popover:close`** | No | `{ popoverId, target, trigger }` | After popover is closed and focus restored back to the trigger. |
79
+
80
+ ```js
81
+ // Example: Cancel open transition for unauthorized zones
82
+ document.addEventListener('ln-popover:before-open', (e) => {
83
+ if (e.detail.popoverId === 'admin-zone' && !currentUser.isAdmin) {
84
+ e.preventDefault(); // Reverts attribute back to "closed"
85
+ }
86
+ });
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 5. Integration Patterns
92
+
93
+ ### A. Position Preferences & Auto-Flip
94
+ Preferred placement is easily configured. If there isn't enough room in the viewport, the positioning engine flips the popover to the opposite side:
95
+ ```html
96
+ <div data-ln-popover data-ln-popover-position="right" id="help-menu">...</div>
97
+ ```
98
+
99
+ ### B. Nested Popovers (A stays open when B opens)
100
+ Opening B from inside A leaves A open. Pressing `ESC` once closes B first, and then A:
101
+ ```html
102
+ <div data-ln-popover id="popover-a">
103
+ <button data-ln-popover-for="popover-b">Open B</button>
104
+ </div>
105
+ <div data-ln-popover id="popover-b">...</div>
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 6. Integration & Source Files
111
+
112
+ - **Unified Bundle**: Loaded automatically with the main bundle:
113
+ ```html
114
+ <script src="dist/ln-ashlar.iife.js" defer></script>
115
+ ```
116
+ - **Standalone IIFE**: For lightweight pages, load the standalone, self-registering IIFE version:
117
+ ```html
118
+ <script src="js/ln-popover/ln-popover.js" defer></script>
119
+ ```
120
+ - **Active Source (ESM)**: Development source is located at [js/ln-popover/src/ln-popover.js](file:///c:/laragon/www/ln-ashlar/js/ln-popover/src/ln-popover.js).
121
+
122
+ ---
123
+
124
+ ## Related
125
+ - **[`ln-dropdown`](../ln-dropdown/README.md)** — Menu wrapper adding click-outside/teleportation.
126
+ - **[`ln-toggle`](../ln-toggle/README.md)** — Binary disclosure state primitive.
127
+ - **Architecture deep-dive** — [`docs/js/popover.md`](../../docs/js/popover.md).
@@ -0,0 +1 @@
1
+ (function(){"use strict";function A(t,e,n){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:n||{}}))}function O(t,e,n){const a=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:n||{}});return t.dispatchEvent(a),a}function k(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){k(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function E(t,e,n,a){if(t.nodeType!==1)return;const p=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",h=Array.from(t.querySelectorAll(p));t.matches&&t.matches(p)&&h.push(t);for(const l of h)l[n]||(l[n]=new a(l))}function I(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function x(t,e,n,a,u={}){const p=u.extraAttributes||[],h=u.onAttributeChange||null,l=u.onInit||null;function d(m){const i=m||document.body;E(i,t,e,n),l&&l(i)}return k(function(){const m=new MutationObserver(function(r){for(let c=0;c<r.length;c++){const s=r[c];if(s.type==="childList")for(let o=0;o<s.addedNodes.length;o++){const g=s.addedNodes[o];g.nodeType===1&&(E(g,t,e,n),l&&l(g))}else s.type==="attributes"&&(h&&s.target[e]?h(s.target,s.attributeName):(E(s.target,t,e,n),l&&l(s.target)))}});let i=[];if(t.indexOf("[")!==-1){const r=/\[([\w-]+)/g;let c;for(;(c=r.exec(t))!==null;)i.push(c[1])}else i.push(t);m.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:i.concat(p)})},a||(t.indexOf("[")===-1?t.replace("data-",""):"component")),window[e]=d,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){d(document.body)}):d(document.body),d}const L={};function R(t,e){L[t]=e}function M(t){return L[t]||{ingress:e=>e,egress:e=>e}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=R,window.lnCore.getDataMapper=M);function T(t,e,n,a){const u=a,p=window.innerWidth,h=window.innerHeight,l=e.width,d=e.height,m=(n||"bottom").split("-"),i=m[0],r=m[1]==="start"||m[1]==="end"?m[1]:"center",c={top:["top","bottom","right","left"],bottom:["bottom","top","right","left"],left:["left","right","top","bottom"],right:["right","left","top","bottom"]},s=c[i]||c.bottom;function o(f){return f==="top"||f==="bottom"?r==="start"?t.left:r==="end"?t.right-l:t.left+(t.width-l)/2:r==="start"?t.top:r==="end"?t.bottom-d:t.top+(t.height-d)/2}function g(f){let _,w,C=!0;return f==="top"?(_=t.top-u-d,w=o(f),_<0&&(C=!1)):f==="bottom"?(_=t.bottom+u,w=o(f),_+d>h&&(C=!1)):f==="left"?(_=o(f),w=t.left-u-l,w<0&&(C=!1)):(_=o(f),w=t.right+u,w+l>p&&(C=!1)),{top:_,left:w,side:f,fits:C}}let v=null;for(let f=0;f<s.length;f++){const _=g(s[f]);if(_.fits){v=_;break}}v||(v=g(s[0]));let b=v.top,y=v.left;return l>=p?y=0:(y<0&&(y=0),y+l>p&&(y=p-l)),d>=h?b=0:(b<0&&(b=0),b+d>h&&(b=h-d)),{top:b,left:y,placement:v.side}}function B(t){if(!t||t.parentNode===document.body)return function(){};const e=t.parentNode,n=document.createComment("ln-teleport");return e.insertBefore(n,t),document.body.appendChild(t),function(){n.parentNode&&(n.parentNode.insertBefore(t,n),n.parentNode.removeChild(n))}}function D(t){if(!t)return{width:0,height:0};const e=t.style,n=e.visibility,a=e.display,u=e.position;e.visibility="hidden",e.display="block",e.position="fixed";const p=t.offsetWidth,h=t.offsetHeight;return e.visibility=n,e.display=a,e.position=u,{width:p,height:h}}(function(){const t="data-ln-popover",e="lnPopover",n="data-ln-popover-for",a="data-ln-popover-position";if(window[e]!==void 0)return;const u=[];let p=null;function h(){p||(p=function(i){if(i.key!=="Escape"||u.length===0)return;u[u.length-1].close()},document.addEventListener("keydown",p))}function l(){u.length>0||p&&(document.removeEventListener("keydown",p),p=null)}function d(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}d.prototype.open=function(i){this.isOpen||(this.trigger=i||null,this.dom.setAttribute(t,"open"))},d.prototype.close=function(){this.isOpen&&this.dom.setAttribute(t,"closed")},d.prototype.toggle=function(i){this.isOpen?this.close():this.open(i)},d.prototype._applyOpen=function(i){this.isOpen=!0,i&&(this.trigger=i),this._previousFocus=document.activeElement,this._teleportRestore=B(this.dom);const r=D(this.dom);if(this.trigger){const g=this.trigger.getBoundingClientRect(),v=this.dom.getAttribute(a)||"bottom",b=T(g,r,v,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 c=this.dom.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'),s=Array.prototype.find.call(c,I);s?s.focus():this.dom.focus();const o=this;this._boundDocClick=function(g){o.dom.contains(g.target)||o.trigger&&o.trigger.contains(g.target)||o.close()},o._docClickTimeout=setTimeout(function(){o._docClickTimeout=null,document.addEventListener("click",o._boundDocClick)},0),this._boundReposition=function(){if(!o.trigger)return;const g=o.trigger.getBoundingClientRect(),v=D(o.dom),b=o.dom.getAttribute(a)||"bottom",y=T(g,v,b,8);o.dom.style.top=y.top+"px",o.dom.style.left=y.left+"px",o.dom.setAttribute("data-ln-popover-placement",y.placement)},window.addEventListener("scroll",this._boundReposition,{passive:!0,capture:!0}),window.addEventListener("resize",this._boundReposition),u.push(this),h(),A(this.dom,"ln-popover:open",{popoverId:this.dom.id,target:this.dom,trigger:this.trigger})},d.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),l(),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},d.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 m(i){this.dom=i;const r=i.getAttribute(n);return i.setAttribute("aria-haspopup","dialog"),i.setAttribute("aria-expanded","false"),i.setAttribute("aria-controls",r),this._onClick=function(c){if(c.ctrlKey||c.metaKey||c.button===1)return;c.preventDefault();const s=document.getElementById(r);!s||!s[e]||s[e].toggle(i)},i.addEventListener("click",this._onClick),this}m.prototype.destroy=function(){this.dom.removeEventListener("click",this._onClick),delete this.dom[e+"Trigger"]},x(t,e,d,"ln-popover",{onAttributeChange:function(i){const r=i[e];if(!r)return;const s=i.getAttribute(t)==="open";if(s!==r.isOpen)if(s){if(O(i,"ln-popover:before-open",{popoverId:i.id,target:i,trigger:r.trigger}).defaultPrevented){i.setAttribute(t,"closed");return}r._applyOpen(r.trigger)}else{if(O(i,"ln-popover:before-close",{popoverId:i.id,target:i,trigger:r.trigger}).defaultPrevented){i.setAttribute(t,"open");return}r._applyClose()}}}),x(n,e+"Trigger",m,"ln-popover-trigger")})()})();
@@ -0,0 +1,288 @@
1
+ import { dispatch, dispatchCancelable, computePlacement, teleportToBody, measureHidden, isVisible, registerComponent } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-popover';
5
+ const DOM_ATTRIBUTE = 'lnPopover';
6
+ const TRIGGER_SELECTOR = 'data-ln-popover-for';
7
+ const POSITION_SELECTOR = 'data-ln-popover-position';
8
+
9
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
10
+
11
+ // ─── Open-stack (Escape closes top of stack) ───────────────
12
+
13
+ const openStack = [];
14
+ let escListener = null;
15
+
16
+ function _ensureEscListener() {
17
+ if (escListener) return;
18
+ escListener = function (e) {
19
+ if (e.key !== 'Escape') return;
20
+ if (openStack.length === 0) return;
21
+ const top = openStack[openStack.length - 1];
22
+ top.close();
23
+ };
24
+ document.addEventListener('keydown', escListener);
25
+ }
26
+
27
+ function _maybeRemoveEscListener() {
28
+ if (openStack.length > 0) return;
29
+ if (!escListener) return;
30
+ document.removeEventListener('keydown', escListener);
31
+ escListener = null;
32
+ }
33
+
34
+ // ─── Component ─────────────────────────────────────────────
35
+
36
+ function _component(dom) {
37
+ this.dom = dom;
38
+ this.isOpen = dom.getAttribute(DOM_SELECTOR) === 'open';
39
+ this.trigger = null;
40
+ this._teleportRestore = null;
41
+ this._previousFocus = null;
42
+ this._boundDocClick = null;
43
+ this._docClickTimeout = null;
44
+ this._boundReposition = null;
45
+
46
+ // Make the popover container itself programmatically focusable
47
+ // as a fallback when it has no focusable children.
48
+ if (!dom.hasAttribute('tabindex')) {
49
+ dom.setAttribute('tabindex', '-1');
50
+ }
51
+ if (!dom.hasAttribute('role')) {
52
+ dom.setAttribute('role', 'dialog');
53
+ }
54
+
55
+ // If the markup says open at boot, sync immediately.
56
+ if (this.isOpen) {
57
+ this._applyOpen(null);
58
+ }
59
+
60
+ return this;
61
+ }
62
+
63
+ _component.prototype.open = function (trigger) {
64
+ if (this.isOpen) return;
65
+ this.trigger = trigger || null;
66
+ this.dom.setAttribute(DOM_SELECTOR, 'open');
67
+ };
68
+
69
+ _component.prototype.close = function () {
70
+ if (!this.isOpen) return;
71
+ this.dom.setAttribute(DOM_SELECTOR, 'closed');
72
+ };
73
+
74
+ _component.prototype.toggle = function (trigger) {
75
+ if (this.isOpen) {
76
+ this.close();
77
+ } else {
78
+ this.open(trigger);
79
+ }
80
+ };
81
+
82
+ // ─── Apply open/close (called from _syncAttribute) ─────────
83
+
84
+ _component.prototype._applyOpen = function (trigger) {
85
+ this.isOpen = true;
86
+ if (trigger) this.trigger = trigger;
87
+ this._previousFocus = document.activeElement;
88
+
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);
92
+
93
+ // Measure (works even though it just got `display: block` by the
94
+ // attribute switch — measureHidden is safe either way).
95
+ const size = measureHidden(this.dom);
96
+
97
+ // Position relative to the trigger (if any).
98
+ if (this.trigger) {
99
+ const rect = this.trigger.getBoundingClientRect();
100
+ const preferred = this.dom.getAttribute(POSITION_SELECTOR) || 'bottom';
101
+ const placement = computePlacement(rect, size, preferred, 8);
102
+ // Inline coordinates are unavoidable for floating UI; CSS
103
+ // supplies position:fixed via the co-located scss, JS only
104
+ // writes `top`/`left`. This is consistent with ln-dropdown.
105
+ this.dom.style.top = placement.top + 'px';
106
+ this.dom.style.left = placement.left + 'px';
107
+ this.dom.setAttribute('data-ln-popover-placement', placement.placement);
108
+ this.trigger.setAttribute('aria-expanded', 'true');
109
+ }
110
+
111
+ // Focus management — first visible focusable, or popover itself.
112
+ const allFocusable = this.dom.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
113
+ const focusable = Array.prototype.find.call(allFocusable, isVisible);
114
+ if (focusable) {
115
+ focusable.focus();
116
+ } else {
117
+ this.dom.focus();
118
+ }
119
+
120
+ // Outside-click listener (delayed by one tick so the opening
121
+ // click doesn't immediately close).
122
+ const self = this;
123
+ this._boundDocClick = function (e) {
124
+ if (self.dom.contains(e.target)) return;
125
+ if (self.trigger && self.trigger.contains(e.target)) return;
126
+ self.close();
127
+ };
128
+ self._docClickTimeout = setTimeout(function () {
129
+ self._docClickTimeout = null;
130
+ document.addEventListener('click', self._boundDocClick);
131
+ }, 0);
132
+
133
+ // Reposition on scroll/resize while open.
134
+ this._boundReposition = function () {
135
+ if (!self.trigger) return;
136
+ const r = self.trigger.getBoundingClientRect();
137
+ const sz = measureHidden(self.dom);
138
+ const preferred = self.dom.getAttribute(POSITION_SELECTOR) || 'bottom';
139
+ const p = computePlacement(r, sz, preferred, 8);
140
+ self.dom.style.top = p.top + 'px';
141
+ self.dom.style.left = p.left + 'px';
142
+ self.dom.setAttribute('data-ln-popover-placement', p.placement);
143
+ };
144
+ window.addEventListener('scroll', this._boundReposition, { passive: true, capture: true });
145
+ window.addEventListener('resize', this._boundReposition);
146
+
147
+ openStack.push(this);
148
+ _ensureEscListener();
149
+
150
+ dispatch(this.dom, 'ln-popover:open', {
151
+ popoverId: this.dom.id,
152
+ target: this.dom,
153
+ trigger: this.trigger
154
+ });
155
+ };
156
+
157
+ _component.prototype._applyClose = function () {
158
+ this.isOpen = false;
159
+
160
+ if (this._docClickTimeout) {
161
+ clearTimeout(this._docClickTimeout);
162
+ this._docClickTimeout = null;
163
+ }
164
+ if (this._boundDocClick) {
165
+ document.removeEventListener('click', this._boundDocClick);
166
+ this._boundDocClick = null;
167
+ }
168
+ if (this._boundReposition) {
169
+ window.removeEventListener('scroll', this._boundReposition, { capture: true });
170
+ window.removeEventListener('resize', this._boundReposition);
171
+ this._boundReposition = null;
172
+ }
173
+
174
+ // Clear positioning inline styles so re-open re-measures cleanly.
175
+ this.dom.style.top = '';
176
+ this.dom.style.left = '';
177
+ this.dom.removeAttribute('data-ln-popover-placement');
178
+
179
+ if (this.trigger) {
180
+ this.trigger.setAttribute('aria-expanded', 'false');
181
+ }
182
+
183
+ // Restore teleport.
184
+ if (this._teleportRestore) {
185
+ this._teleportRestore();
186
+ this._teleportRestore = null;
187
+ }
188
+
189
+ // Remove from open stack.
190
+ const idx = openStack.indexOf(this);
191
+ if (idx !== -1) openStack.splice(idx, 1);
192
+ _maybeRemoveEscListener();
193
+
194
+ // Restore focus to trigger if Escape closed it; on outside-click
195
+ // we deliberately don't yank focus.
196
+ if (this._previousFocus && this.trigger && this._previousFocus === this.trigger) {
197
+ this.trigger.focus();
198
+ } else if (this.trigger && document.activeElement === document.body) {
199
+ // User dismissed via Escape — focus was inside popover; return it.
200
+ this.trigger.focus();
201
+ }
202
+ this._previousFocus = null;
203
+
204
+ dispatch(this.dom, 'ln-popover:close', {
205
+ popoverId: this.dom.id,
206
+ target: this.dom,
207
+ trigger: this.trigger
208
+ });
209
+ this.trigger = null;
210
+ };
211
+
212
+ _component.prototype.destroy = function () {
213
+ if (!this.dom[DOM_ATTRIBUTE]) return;
214
+ if (this.isOpen) this._applyClose();
215
+ delete this.dom[DOM_ATTRIBUTE];
216
+ dispatch(this.dom, 'ln-popover:destroyed', {
217
+ popoverId: this.dom.id,
218
+ target: this.dom
219
+ });
220
+ };
221
+
222
+ // ─── Trigger Component ─────────────────────────────────────
223
+
224
+ function _triggerComponent(dom) {
225
+ this.dom = dom;
226
+ const popoverId = dom.getAttribute(TRIGGER_SELECTOR);
227
+ dom.setAttribute('aria-haspopup', 'dialog');
228
+ dom.setAttribute('aria-expanded', 'false');
229
+ dom.setAttribute('aria-controls', popoverId);
230
+
231
+ const self = this;
232
+ this._onClick = function (e) {
233
+ if (e.ctrlKey || e.metaKey || e.button === 1) return;
234
+ e.preventDefault();
235
+ const target = document.getElementById(popoverId);
236
+ if (!target || !target[DOM_ATTRIBUTE]) return;
237
+ target[DOM_ATTRIBUTE].toggle(dom);
238
+ };
239
+ dom.addEventListener('click', this._onClick);
240
+ return this;
241
+ }
242
+
243
+ _triggerComponent.prototype.destroy = function () {
244
+ this.dom.removeEventListener('click', this._onClick);
245
+ delete this.dom[DOM_ATTRIBUTE + 'Trigger'];
246
+ };
247
+
248
+ // ─── Registration ──────────────────────────────────────────
249
+
250
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-popover', {
251
+ onAttributeChange: function (el) {
252
+ const instance = el[DOM_ATTRIBUTE];
253
+ if (!instance) return;
254
+
255
+ const value = el.getAttribute(DOM_SELECTOR);
256
+ const shouldBeOpen = value === 'open';
257
+
258
+ if (shouldBeOpen === instance.isOpen) return;
259
+
260
+ if (shouldBeOpen) {
261
+ const before = dispatchCancelable(el, 'ln-popover:before-open', {
262
+ popoverId: el.id,
263
+ target: el,
264
+ trigger: instance.trigger
265
+ });
266
+ if (before.defaultPrevented) {
267
+ el.setAttribute(DOM_SELECTOR, 'closed');
268
+ return;
269
+ }
270
+ instance._applyOpen(instance.trigger);
271
+ } else {
272
+ const before = dispatchCancelable(el, 'ln-popover:before-close', {
273
+ popoverId: el.id,
274
+ target: el,
275
+ trigger: instance.trigger
276
+ });
277
+ if (before.defaultPrevented) {
278
+ el.setAttribute(DOM_SELECTOR, 'open');
279
+ return;
280
+ }
281
+ instance._applyClose();
282
+ }
283
+ }
284
+ });
285
+
286
+ registerComponent(TRIGGER_SELECTOR, DOM_ATTRIBUTE + 'Trigger', _triggerComponent, 'ln-popover-trigger');
287
+ })();
288
+