@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
@@ -8,40 +8,7 @@ import { persistGet, persistSet } from '../../ln-core';
8
8
  if (window[DOM_ATTRIBUTE] !== undefined) return;
9
9
 
10
10
 
11
- function _attachTriggers(root) {
12
- const triggers = Array.from(root.querySelectorAll('[data-ln-toggle-for]'));
13
- if (root.hasAttribute && root.hasAttribute('data-ln-toggle-for')) {
14
- triggers.push(root);
15
- }
16
- for (const btn of triggers) {
17
- if (btn[DOM_ATTRIBUTE + 'Trigger']) continue;
18
- const handler = function (e) {
19
- if (e.ctrlKey || e.metaKey || e.button === 1) return;
20
- e.preventDefault();
21
- const targetId = btn.getAttribute('data-ln-toggle-for');
22
- const target = document.getElementById(targetId);
23
- if (!target || !target[DOM_ATTRIBUTE]) return;
24
11
 
25
- const action = btn.getAttribute('data-ln-toggle-action') || 'toggle';
26
- if (action === 'open') {
27
- target.setAttribute(DOM_SELECTOR, 'open');
28
- } else if (action === 'close') {
29
- target.setAttribute(DOM_SELECTOR, 'close');
30
- } else if (action === 'toggle') {
31
- const current = target.getAttribute(DOM_SELECTOR);
32
- target.setAttribute(DOM_SELECTOR, current === 'open' ? 'close' : 'open');
33
- }
34
- // unknown action — silent no-op
35
- };
36
- btn.addEventListener('click', handler);
37
- btn[DOM_ATTRIBUTE + 'Trigger'] = handler;
38
- const targetId = btn.getAttribute('data-ln-toggle-for');
39
- const target = document.getElementById(targetId);
40
- if (target && target[DOM_ATTRIBUTE]) {
41
- btn.setAttribute('aria-expanded', target[DOM_ATTRIBUTE].isOpen ? 'true' : 'false');
42
- }
43
- }
44
- }
45
12
 
46
13
  function _syncTriggerAria(panelEl, isOpen) {
47
14
  const triggers = document.querySelectorAll(
@@ -57,6 +24,21 @@ import { persistGet, persistSet } from '../../ln-core';
57
24
  function _component(dom) {
58
25
  this.dom = dom;
59
26
 
27
+ const self = this;
28
+ this._onRequestOpen = function () {
29
+ self.open();
30
+ };
31
+ this._onRequestClose = function () {
32
+ self.close();
33
+ };
34
+ this._onRequestToggle = function () {
35
+ self.toggle();
36
+ };
37
+
38
+ this.dom.addEventListener('ln-toggle:request-open', this._onRequestOpen);
39
+ this.dom.addEventListener('ln-toggle:request-close', this._onRequestClose);
40
+ this.dom.addEventListener('ln-toggle:request-toggle', this._onRequestToggle);
41
+
60
42
  // ─── Restore persisted state ──────────────────────────────
61
43
  if (dom.hasAttribute('data-ln-persist')) {
62
44
  const saved = persistGet('toggle', dom);
@@ -76,16 +58,25 @@ import { persistGet, persistSet } from '../../ln-core';
76
58
  return this;
77
59
  }
78
60
 
61
+ _component.prototype.open = function () {
62
+ this.dom.setAttribute(DOM_SELECTOR, 'open');
63
+ };
64
+
65
+ _component.prototype.close = function () {
66
+ this.dom.setAttribute(DOM_SELECTOR, 'close');
67
+ };
68
+
69
+ _component.prototype.toggle = function () {
70
+ const current = this.dom.getAttribute(DOM_SELECTOR);
71
+ this.dom.setAttribute(DOM_SELECTOR, current === 'open' ? 'close' : 'open');
72
+ };
73
+
79
74
  _component.prototype.destroy = function () {
80
75
  if (!this.dom[DOM_ATTRIBUTE]) return;
76
+ this.dom.removeEventListener('ln-toggle:request-open', this._onRequestOpen);
77
+ this.dom.removeEventListener('ln-toggle:request-close', this._onRequestClose);
78
+ this.dom.removeEventListener('ln-toggle:request-toggle', this._onRequestToggle);
81
79
  dispatch(this.dom, 'ln-toggle:destroyed', { target: this.dom });
82
- const triggers = document.querySelectorAll('[data-ln-toggle-for="' + this.dom.id + '"]');
83
- for (const btn of triggers) {
84
- if (btn[DOM_ATTRIBUTE + 'Trigger']) {
85
- btn.removeEventListener('click', btn[DOM_ATTRIBUTE + 'Trigger']);
86
- delete btn[DOM_ATTRIBUTE + 'Trigger'];
87
- }
88
- }
89
80
  delete this.dom[DOM_ATTRIBUTE];
90
81
  };
91
82
 
@@ -129,11 +120,33 @@ import { persistGet, persistSet } from '../../ln-core';
129
120
  }
130
121
  }
131
122
 
123
+ // ─── Event Delegation ──────────────────────────────────────
124
+
125
+ document.addEventListener('click', function (e) {
126
+ if (e.ctrlKey || e.metaKey || e.button === 1) return;
127
+
128
+ const trigger = e.target.closest('[data-ln-toggle-for]');
129
+ if (trigger) {
130
+ const targetId = trigger.getAttribute('data-ln-toggle-for');
131
+ const target = document.getElementById(targetId);
132
+ if (target && target[DOM_ATTRIBUTE]) {
133
+ e.preventDefault();
134
+ const action = trigger.getAttribute('data-ln-toggle-action') || 'toggle';
135
+ if (action === 'open') {
136
+ target.setAttribute(DOM_SELECTOR, 'open');
137
+ } else if (action === 'close') {
138
+ target.setAttribute(DOM_SELECTOR, 'close');
139
+ } else if (action === 'toggle') {
140
+ const current = target.getAttribute(DOM_SELECTOR);
141
+ target.setAttribute(DOM_SELECTOR, current === 'open' ? 'close' : 'open');
142
+ }
143
+ }
144
+ }
145
+ });
146
+
132
147
  // ─── Init ──────────────────────────────────────────────────
133
148
 
134
149
  registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-toggle', {
135
- extraAttributes: ['data-ln-toggle-for'],
136
- onAttributeChange: _syncAttribute,
137
- onInit: _attachTriggers
150
+ onAttributeChange: _syncAttribute
138
151
  });
139
152
  })();
@@ -1,58 +1,95 @@
1
- # ln-tooltip
2
-
3
- A zero-dependency, progressively enhanced **Dual-Layer Tooltip Primitive** that displays lightweight contextual descriptions on hover and focus.
4
-
5
- It supports two levels of execution: a **pure CSS baseline** (zero JS footprint, utilizing pseudo-elements) and a **JS progressive enhancement layer** (portaled to the body to escape parent clipping, viewport-aware auto-flipping, and automated `aria-describedby` wiring).
6
-
7
- ---
8
-
9
- ## 🧭 Philosophy & Architecture
10
-
11
- 1. **CSS-First Baseline (Zero-JS):** Every element carrying `data-ln-tooltip="text"` immediately receives a beautiful hover/focus visual tooltip via pure CSS `::after` pseudo-elements.
12
- 2. **JS Portaled Enhancement (`data-ln-tooltip-enhance`):** Opt-in to activate JS features. The component detaches the tooltip from the trigger and mounts it in a global `<body>` portal (`#ln-tooltip-portal`), avoiding parent `overflow: hidden` clipping, wrapping long texts safely, and auto-flipping the bubble if it hits viewport boundaries.
13
- 3. **Automated `<abbr>` Semantic Integration:** Elements containing both `data-ln-tooltip` and a native `title` attribute (such as standard `<abbr>` elements) auto-enhance without requiring the `-enhance` flag. The JS layer intercepts the native browser hover tooltip and replaces it dynamically to avoid double tooltips.
14
-
15
- ---
16
-
17
- ## 📦 Minimal Blueprint
18
-
19
- ### CSS Baseline (Zero JS)
20
- ```html
21
- <button type="button" data-ln-tooltip="Save document" aria-label="Save document">
22
- <svg class="ln-icon" aria-hidden="true"><use href="#ln-device-floppy"></use></svg>
23
- </button>
24
- ```
25
-
26
- ### JS Progressive Enhancement (Viewport-Aware & Portaled)
27
- Add `data-ln-tooltip-enhance` to activate advanced positioning and accessibility wiring.
28
- ```html
29
- <button type="button"
30
- data-ln-tooltip="Delete this document permanently"
31
- data-ln-tooltip-enhance
32
- data-ln-tooltip-position="right"
33
- aria-label="Delete document">
34
- <svg class="ln-icon" aria-hidden="true"><use href="#ln-trash"></use></svg>
35
- </button>
36
- ```
37
-
38
- ---
39
-
40
- ## 🛠️ Declarative API Contract
41
-
42
- ### HTML Attributes
43
-
44
- | Attribute | Elements | Description |
45
- | :--- | :--- | :--- |
46
- | `data-ln-tooltip="text"` | Trigger element | Tooltip text. Required. If empty, falls back to the native `title` attribute. |
47
- | `data-ln-tooltip-position` | Trigger element | Preferred placement side: `top` (default), `bottom`, `left`, `right`. |
48
- | `data-ln-tooltip-enhance` | Trigger element | Opt-in. Activates JS portaling, edge auto-flipping, and accessibility descriptions. |
49
- | `title` | Trigger element | When present alongside `data-ln-tooltip`, forces auto-enhance to suppress native tooltips. |
50
- | `aria-describedby` | Trigger element | *State*. Automatically wired by the JS layer at runtime to point to the portal bubble ID. |
51
-
52
- ---
53
-
54
- ## ⚠️ Common Pitfalls
55
-
56
- - **Omitting `aria-label` on Icon Buttons:** Sighted users see the tooltip, but screen readers require standard labeling. Sighted tooltips are visual mirrors; always include a matching `aria-label` on icon-only controls.
57
- - **Triggering on Non-Focusable Elements:** Tooltips rely on hover and keyboard focus. Putting `data-ln-tooltip` on plain `<span>` or `<div>` elements without `tabindex="0"` makes them completely inaccessible to keyboard users.
58
- - **Applying to Native Disabled Buttons:** Standard disabled buttons (e.g. `<button disabled>`) block pointer events in many browsers, preventing tooltips from firing. Use `aria-disabled="true"` instead to preserve tooltips while indicating disabled status.
1
+ # ln-tooltip
2
+
3
+ A zero-dependency, progressively enhanced **Dual-Layer Tooltip Primitive** that displays lightweight contextual descriptions on hover and focus.
4
+
5
+ It supports two levels of execution: a **pure CSS baseline** (zero JS footprint, utilizing pseudo-elements) and a **JS progressive enhancement layer** (top-layer promoted via the native Popover API to escape parent clipping, viewport-aware auto-flipping, and automated `aria-describedby` wiring).
6
+
7
+ ---
8
+
9
+ ## 🧭 Philosophy & Architecture
10
+
11
+ 1. **CSS-First Baseline (Zero-JS):** Every element carrying `data-ln-tooltip="text"` immediately receives a beautiful hover/focus visual tooltip via pure CSS `::after` pseudo-elements.
12
+ 2. **JS Top-Layer Enhancement (`data-ln-tooltip-enhance`):** Opt-in to activate JS features. The component detaches the tooltip from the trigger and mounts it in a shared `<body>` container (`#ln-tooltip-portal`) promoted to the browser's top layer via the native Popover API (`popover="manual"`, `showPopover()`/`hidePopover()`), avoiding parent `overflow: hidden` clipping and any ancestor stacking context, wrapping long texts safely, and auto-flipping the bubble if it hits viewport boundaries.
13
+ 3. **Automated `<abbr>` Semantic Integration:** Elements containing both `data-ln-tooltip` and a native `title` attribute (such as standard `<abbr>` elements) auto-enhance without requiring the `-enhance` flag. The JS layer intercepts the native browser hover tooltip and replaces it dynamically to avoid double tooltips.
14
+
15
+ ---
16
+
17
+ ## 📦 Minimal Blueprint
18
+
19
+ ### CSS Baseline (Zero JS)
20
+ ```html
21
+ <button type="button" data-ln-tooltip="Save document" aria-label="Save document">
22
+ <svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-device-floppy"></use></svg>
23
+ </button>
24
+ ```
25
+
26
+ ### JS Progressive Enhancement (Viewport-Aware & Top-Layer Promoted)
27
+ Add `data-ln-tooltip-enhance` to activate advanced positioning and accessibility wiring.
28
+ ```html
29
+ <button type="button"
30
+ data-ln-tooltip="Delete this document permanently"
31
+ data-ln-tooltip-enhance
32
+ data-ln-tooltip-position="right"
33
+ aria-label="Delete document">
34
+ <svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-trash"></use></svg>
35
+ </button>
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 🛠️ Declarative API Contract
41
+
42
+ ### HTML Attributes
43
+
44
+ | Attribute | Elements | Description |
45
+ | :--- | :--- | :--- |
46
+ | `data-ln-tooltip="text"` | Trigger element | Tooltip text. Required. If empty, falls back to the native `title` attribute. |
47
+ | `data-ln-tooltip-position` | Trigger element | Preferred placement side: `top` (default), `bottom`, `left`, `right`. |
48
+ | `data-ln-tooltip-enhance` | Trigger element | Opt-in. Activates top-layer promotion, edge auto-flipping, and accessibility descriptions. |
49
+ | `title` | Trigger element | When present alongside `data-ln-tooltip`, forces auto-enhance to suppress native tooltips. |
50
+ | `aria-describedby` | Trigger element | *State*. Automatically wired by the JS layer at runtime to point to the portal bubble ID. |
51
+
52
+ ---
53
+
54
+ ## ⚠️ Common Pitfalls
55
+
56
+ - **Omitting `aria-label` on Icon Buttons:** Sighted users see the tooltip, but screen readers require standard labeling. Sighted tooltips are visual mirrors; always include a matching `aria-label` on icon-only controls.
57
+ - **Triggering on Non-Focusable Elements:** Tooltips rely on hover and keyboard focus. Putting `data-ln-tooltip` on plain `<span>` or `<div>` elements without `tabindex="0"` makes them completely inaccessible to keyboard users.
58
+ - **Applying to Native Disabled Buttons:** Standard disabled buttons (e.g. `<button disabled>`) block pointer events in many browsers, preventing tooltips from firing. Use `aria-disabled="true"` instead to preserve tooltips while indicating disabled status.
59
+
60
+ ---
61
+
62
+ ## 🔧 Internals
63
+
64
+ Source: `js/ln-tooltip/ln-tooltip.js`. Tooltips are mechanically inert from the data layer's perspective — no coordinator wiring, the only event dispatched is `ln-tooltip:destroyed`.
65
+
66
+ ### Two render strategies, one attribute
67
+
68
+ `scss/components/_tooltip.scss` applies `@mixin tooltip` to every `[data-ln-tooltip]` element unconditionally — a `::after` pseudo-element reading `attr(data-ln-tooltip)` (falling back to `attr(title)`), toggled on `:hover`/`:focus-visible`. Zero JS. The JS layer attaches only when an element matches `[data-ln-tooltip-enhance]` (explicit opt-in) or `[data-ln-tooltip][title]` (auto-enhance, so the CSS baseline can't otherwise stop the native `title` tooltip from leaking through). For either match, co-located CSS sets `content: none` on the `::after`, suppressing the baseline so only one bubble ever renders:
69
+
70
+ ```scss
71
+ [data-ln-tooltip][data-ln-tooltip-enhance]::after,
72
+ [data-ln-tooltip][title]::after { content: none; }
73
+ ```
74
+
75
+ This suppressor is static CSS, in effect before JS executes — no flash of the baseline pre-init. If JS never runs, an auto-enhance element (has `title`) degrades to the browser's native tooltip; an explicit `-enhance` element with no `title` degrades to nothing, an accepted cost of that opt-in.
76
+
77
+ ### Portal architecture
78
+
79
+ A single `<div id="ln-tooltip-portal">` is created lazily in `<body>` on the first `_show` call — `position: fixed` at `(0,0)`, `pointer-events: none`, `z-index: var(--z-toast)` (shares the toast layer, so tooltips render above modals/dropdowns regardless of ancestor stacking context). It is never destroyed. Each tooltip node is its own `position: fixed` `<div class="ln-tooltip">`, so its placement is independent of the portal's box — only one node exists inside the portal at any time, created on show, removed on hide.
80
+
81
+ ### Show / hide flow
82
+
83
+ `_show(trigger)`: same-trigger guard (a second `mouseenter` from a hovered child doesn't recreate the bubble) → `_hide()` any previous tooltip → read `data-ln-tooltip` (fallback `title`), abort silently if both empty → ensure portal → stash and remove `title` (prevents the native browser tooltip appearing after ~1s hover dwell; restored on hide) → build the node with a stable id and append it (must be in the DOM before `computePlacement` reads `offsetWidth`/`offsetHeight`) → `computePlacement(rect, size, preferred, 6)` returns `{ top, left, placement }` after viewport auto-flip → write the two inline styles + `data-ln-tooltip-placement` → wire `aria-describedby` → register the ESC listener if not already present.
84
+
85
+ `_hide()`: no-op if nothing is active; otherwise restores the stashed `title`, clears `aria-describedby`, removes the node from the DOM (not cached), clears the active-state variables, and removes the ESC listener.
86
+
87
+ ### One visible at a time
88
+
89
+ Module-level `activeTrigger`/`activeTooltipNode` enforce a hard single-tooltip invariant — every `_show` hides the previous bubble first, mirroring the CSS baseline's natural one-hover-at-a-time behavior. This is also why the `title` stash can live in a single module-level variable rather than a per-trigger map.
90
+
91
+ ### Lifecycle
92
+
93
+ `registerComponent` wires a `MutationObserver` watching for `[data-ln-tooltip-enhance], [data-ln-tooltip][title]`. On init, four listeners attach to the trigger: `mouseenter`/`focus` → `_show(el)`; `mouseleave`/`blur` → `_hide()` guarded by `activeTrigger === el` (so one trigger can't hide another's tooltip). `focus`/`blur` use the capture phase so events from focusable descendants reach the trigger reliably.
94
+
95
+ The component never auto-destroys on element removal — listeners are garbage-collected with the element. `destroy()` exists for the rarer case of unwiring without removing the element: removes all four listeners, `_hide()`s if this trigger is currently active, deletes the instance properties, dispatches `ln-tooltip:destroyed`.
@@ -0,0 +1,9 @@
1
+ @use '../../scss/config/mixins' as *;
2
+
3
+ [data-ln-debug] {
4
+ // 1. Empty Tooltip: accessibility warning
5
+ // Flags a [data-ln-tooltip] declared empty with no native title fallback (renders an empty bubble); :not([title]) honors the documented title fallback
6
+ [data-ln-tooltip=""]:not([title]) {
7
+ @include dev-inline-error("data-ln-tooltip is empty and has no title fallback");
8
+ }
9
+ }
@@ -1 +1 @@
1
- (function(){"use strict";function x(e,o,b){e.dispatchEvent(new CustomEvent(o,{bubbles:!0,detail:b||{}}))}function C(e,o){if(!document.body){document.addEventListener("DOMContentLoaded",function(){C(e,o)}),console.warn("["+o+'] Script loaded before <body> — add "defer" to your <script> tag');return}e()}function L(e,o,b,c){if(e.nodeType!==1)return;const y=o.indexOf("[")!==-1||o.indexOf(".")!==-1||o.indexOf("#")!==-1?o:"["+o+"]",r=Array.from(e.querySelectorAll(y));e.matches&&e.matches(y)&&r.push(e);for(const i of r)i[b]||(i[b]=new c(i))}function O(e,o,b,c,v={}){const y=v.extraAttributes||[],r=v.onAttributeChange||null,i=v.onInit||null;function s(p){const f=p||document.body;L(f,e,o,b),i&&i(f)}return C(function(){const p=new MutationObserver(function(w){for(let E=0;E<w.length;E++){const d=w[E];if(d.type==="childList"){for(let u=0;u<d.addedNodes.length;u++){const l=d.addedNodes[u];l.nodeType===1&&(L(l,e,o,b),i&&i(l))}for(let u=0;u<d.removedNodes.length;u++){const l=d.removedNodes[u];if(l.nodeType===1){const t=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",m=Array.from(l.querySelectorAll(t));l.matches&&l.matches(t)&&m.push(l);for(let n=0;n<m.length;n++){const a=m[n];if(!document.contains(a)){const h=a[o];h&&typeof h.destroy=="function"&&h.destroy()}}}}}else d.type==="attributes"&&(r&&d.target[o]?r(d.target,d.attributeName):(L(d.target,e,o,b),i&&i(d.target)))}});let f=[];if(e.indexOf("[")!==-1){const w=/\[([\w-]+)/g;let E;for(;(E=w.exec(e))!==null;)f.push(E[1])}else f.push(e);p.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:f.concat(y)})},c),window[o]=s,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){s(document.body)}):s(document.body),s}const T={};function N(e,o){T[e]=o}function M(e){return T[e]||{ingress:o=>o,egress:o=>o}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=N,window.lnCore.getDataMapper=M);function D(e,o,b,c){const v=c,y=window.innerWidth,r=window.innerHeight,i=o.width,s=o.height,p=b.split("-"),f=p[0],w=p[1]==="start"||p[1]==="end"?p[1]:"center",E={top:["top","bottom","right","left"],bottom:["bottom","top","right","left"],left:["left","right","top","bottom"],right:["right","left","top","bottom"]},d=E[f]||E.bottom;function u(n){return n==="top"||n==="bottom"?w==="start"?e.left:w==="end"?e.right-i:e.left+(e.width-i)/2:w==="start"?e.top:w==="end"?e.bottom-s:e.top+(e.height-s)/2}function l(n){let a,h,_=!0;return n==="top"?(a=e.top-v-s,h=u(n),a<0&&(_=!1)):n==="bottom"?(a=e.bottom+v,h=u(n),a+s>r&&(_=!1)):n==="left"?(a=u(n),h=e.left-v-i,h<0&&(_=!1)):(a=u(n),h=e.right+v,h+i>y&&(_=!1)),{top:a,left:h,side:n,fits:_}}let A=null;for(let n=0;n<d.length;n++){const a=l(d[n]);if(a.fits){A=a;break}}A||(A=l(d[0]));let t=A.top,m=A.left;return i>=y?m=0:(m<0&&(m=0),m+i>y&&(m=y-i)),s>=r?t=0:(t<0&&(t=0),t+s>r&&(t=r-s)),{top:t,left:m,placement:A.side}}(function(){const e="data-ln-tooltip-enhance",o="data-ln-tooltip",b="data-ln-tooltip-position",c="lnTooltipEnhance",v="ln-tooltip-portal";if(window[c]!==void 0)return;let y=0,r=null,i=null,s=null,p=null,f=null;function w(){return r&&r.parentNode||(r=document.getElementById(v),r||(r=document.createElement("div"),r.id=v,document.body.appendChild(r))),r}function E(){f||(f=function(t){t.key==="Escape"&&l()},document.addEventListener("keydown",f))}function d(){f&&(document.removeEventListener("keydown",f),f=null)}function u(t){if(s===t)return;l();const m=t.getAttribute(o)||t.getAttribute("title");if(!m)return;w(),t.hasAttribute("title")&&(p=t.getAttribute("title"),t.removeAttribute("title"));const n=document.createElement("div");n.className="ln-tooltip",n.textContent=m,t[c+"Uid"]||(y+=1,t[c+"Uid"]="ln-tooltip-"+y),n.id=t[c+"Uid"],r.appendChild(n);const a=n.offsetWidth,h=n.offsetHeight,_=t.getBoundingClientRect(),I=t.getAttribute(b)||"top",g=D(_,{width:a,height:h},I,6);n.style.top=g.top+"px",n.style.left=g.left+"px",n.setAttribute("data-ln-tooltip-placement",g.placement),t.setAttribute("aria-describedby",n.id),i=n,s=t,E()}function l(){if(!i){d();return}s&&(s.removeAttribute("aria-describedby"),p!==null&&s.setAttribute("title",p)),p=null,i.parentNode&&i.parentNode.removeChild(i),i=null,s=null,d()}function A(t){return this.dom=t,t.hasAttribute("data-ln-tooltip-enhanced")||(t.setAttribute("data-ln-tooltip-enhanced",""),this._addedEnhancedAttr=!0),this._onEnter=function(){u(t)},this._onLeave=function(){s===t&&l()},this._onFocus=function(){u(t)},this._onBlur=function(){s===t&&l()},t.addEventListener("mouseenter",this._onEnter),t.addEventListener("mouseleave",this._onLeave),t.addEventListener("focus",this._onFocus,!0),t.addEventListener("blur",this._onBlur,!0),this}A.prototype.destroy=function(){const t=this.dom;t.removeEventListener("mouseenter",this._onEnter),t.removeEventListener("mouseleave",this._onLeave),t.removeEventListener("focus",this._onFocus,!0),t.removeEventListener("blur",this._onBlur,!0),s===t&&l(),this._addedEnhancedAttr&&t.removeAttribute("data-ln-tooltip-enhanced"),delete t[c],delete t[c+"Uid"],x(t,"ln-tooltip:destroyed",{trigger:t})},O("["+e+"], ["+o+"][title]",c,A,"ln-tooltip")})()})();
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 E={};function B(t,e){E[t]||(E[t]=document.querySelector('[data-ln-template="'+t+'"]'));const r=E[t];return r?r.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function I(t,e,r){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:r||{}}))}function T(t,e){if(!t||!e)return t;const r=t.querySelectorAll("[data-ln-field]");for(let n=0;n<r.length;n++){const l=r[n],o=l.getAttribute("data-ln-field");e[o]!=null&&(l.textContent=e[o])}const c=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<c.length;n++){const l=c[n],o=l.getAttribute("data-ln-attr").split(",");for(let s=0;s<o.length;s++){const d=o[s].trim().split(":");if(d.length!==2)continue;const a=d[0].trim(),m=d[1].trim();e[m]!=null&&l.setAttribute(a,e[m])}}const f=t.querySelectorAll("[data-ln-show]");for(let n=0;n<f.length;n++){const l=f[n],o=l.getAttribute("data-ln-show");o in e&&l.classList.toggle("hidden",!e[o])}const u=t.querySelectorAll("[data-ln-class]");for(let n=0;n<u.length;n++){const l=u[n],o=l.getAttribute("data-ln-class").split(",");for(let s=0;s<o.length;s++){const d=o[s].trim().split(":");if(d.length!==2)continue;const a=d[0].trim(),m=d[1].trim();m in e&&l.classList.toggle(a,!!e[m])}}return t}function P(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const r=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let c=0;c<r.length;c++)r[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)T(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let r=0;r<e.length;r++)e[r].textContent=""}})));function S(t,e){if(!t||!e)return t;const r=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;r.nextNode();){const u=r.currentNode;u.textContent.indexOf("{{")!==-1&&(u.textContent=u.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,l){return e[l]!==void 0?e[l]:""}))}const c=function(u,n){return e[n]!==void 0?e[n]:""},f=Array.from(t.querySelectorAll("*"));t.nodeType===1&&f.push(t);for(let u=0;u<f.length;u++){const n=f[u],l=n.attributes;for(let o=0;o<l.length;o++){const s=l[o];s.value.indexOf("{{")!==-1&&n.setAttribute(s.name,s.value.replace(/\{\{\s*(\w+)\s*\}\}/g,c))}}return t}function F(t,e,r,c,f,u){const n={};for(let o=0;o<t.children.length;o++){const s=t.children[o],d=s.getAttribute("data-ln-key");d&&(n[d]=s)}const l=document.createDocumentFragment();for(let o=0;o<e.length;o++){const s=e[o],d=String(c(s));let a=n[d];if(a)f(a,s,o);else{const m=B(r,u);if(!m||(S(m,s),a=m.firstElementChild,!a))continue;a.setAttribute("data-ln-key",d),f(a,s,o)}l.appendChild(a)}t.textContent="",t.appendChild(l)}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 L(t,e,r,c){if(t.nodeType!==1)return;const u=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(u));t.matches&&t.matches(u)&&n.push(t);for(const l of n)l[r]||(l[r]=new c(l))}function M(t,e,r,c,f={}){const u=f.extraAttributes||[],n=f.onAttributeChange||null,l=f.onInit||null;function o(s){const d=s||document.body;L(d,t,e,r),l&&l(d)}return k(function(){const s=new MutationObserver(function(a){for(let m=0;m<a.length;m++){const b=a[m];if(b.type==="childList"){for(let y=0;y<b.addedNodes.length;y++){const g=b.addedNodes[y];g.nodeType===1&&(L(g,t,e,r),l&&l(g))}for(let y=0;y<b.removedNodes.length;y++){const g=b.removedNodes[y];if(g.nodeType===1){const C=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",i=Array.from(g.querySelectorAll(C));g.matches&&g.matches(C)&&i.push(g);for(let h=0;h<i.length;h++){const w=i[h];if(!document.contains(w)){const p=w[e];p&&typeof p.destroy=="function"&&p.destroy()}}}}}else b.type==="attributes"&&(n&&b.target[e]?n(b.target,b.attributeName):(L(b.target,t,e,r),l&&l(b.target)))}});let d=[];if(t.indexOf("[")!==-1){const a=/\[([\w-]+)/g;let m;for(;(m=a.exec(t))!==null;)d.push(m[1])}else d.push(t);s.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:d.concat(u)})},c),window[e]=o,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){o(document.body)}):o(document.body),o}const O={};function W(t,e){O[t]=e}function j(t){return O[t]||{ingress:e=>e,egress:e=>e}}const N={};function D(t,e){if(!t||typeof e!="object")return;const r=t.toLowerCase().split("-")[0];N[r]=e}function U(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return N[e]||null}D("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=W,window.lnCore.getDataMapper=j,window.lnCore.registerLocaleFallback=D,window.lnCore.getLocaleFallback=U,window.lnCore.fillTemplate=S,window.lnCore.fill=T,window.lnCore.lnFill=P,window.lnCore.renderList=F);function q(t){return(t||"").replace(/^#/,"")}function _(t){const e=t===void 0?location.hash:t,r={},c=q(e);if(!c)return r;const f=c.split("&");for(let u=0;u<f.length;u++){const n=f[u];if(!n)continue;const l=n.indexOf(":"),o=l>-1?n.slice(0,l):n,s=l>-1?n.slice(l+1):"";if(o)try{r[o]=decodeURIComponent(s)}catch{r[o]=s}}return r}function G(t){if(!t)return null;const e=_();return t in e?e[t]:null}function K(t,e){if(!t)return;const r=_();e==null?delete r[t]:r[t]=String(e);const f=Object.keys(r).map(function(u){const n=r[u];return n===""?u:u+":"+encodeURIComponent(n)}).join("&");q(location.hash)!==f&&(location.hash=f)}function H(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=_,window.lnCore.hashGet=G,window.lnCore.hashSet=K,window.lnCore.hashLinkClick=H);function X(t,e,r,c){const f=c,u=window.innerWidth,n=window.innerHeight,l=e.width,o=e.height,s=r.split("-"),d=s[0],a=s[1]==="start"||s[1]==="end"?s[1]:"center",m={top:["top","bottom","right","left"],bottom:["bottom","top","right","left"],left:["left","right","top","bottom"],right:["right","left","top","bottom"]},b=m[d]||m.bottom;function y(h){return h==="top"||h==="bottom"?a==="start"?t.left:a==="end"?t.right-l:t.left+(t.width-l)/2:a==="start"?t.top:a==="end"?t.bottom-o:t.top+(t.height-o)/2}function g(h){let w,p,A=!0;return h==="top"?(w=t.top-f-o,p=y(h),w<0&&(A=!1)):h==="bottom"?(w=t.bottom+f,p=y(h),w+o>n&&(A=!1)):h==="left"?(w=y(h),p=t.left-f-l,p<0&&(A=!1)):(w=y(h),p=t.right+f,p+l>u&&(A=!1)),{top:w,left:p,side:h,fits:A}}let v=null;for(let h=0;h<b.length;h++){const w=g(b[h]);if(w.fits){v=w;break}}v||(v=g(b[0]));let C=v.top,i=v.left;return l>=u?i=0:(i<0&&(i=0),i+l>u&&(i=u-l)),o>=n?C=0:(C<0&&(C=0),C+o>n&&(C=n-o)),{top:C,left:i,placement:v.side}}(function(){const t="data-ln-tooltip-enhance",e="data-ln-tooltip",r="data-ln-tooltip-position",c="lnTooltipEnhance",f="ln-tooltip-portal";if(window[c]!==void 0)return;let u=0,n=null,l=null,o=null,s=null,d=null,a=null;function m(){return n&&n.parentNode||(n=document.getElementById(f),n||(n=document.createElement("div"),n.id=f,document.body.appendChild(n)),n.hasAttribute("popover")||n.setAttribute("popover","manual")),n}function b(){a||(a=function(i){i.key==="Escape"&&v()},document.addEventListener("keydown",a))}function y(){a&&(document.removeEventListener("keydown",a),a=null)}function g(i){if(o===i)return;v();const h=i.getAttribute(e)||i.getAttribute("title");if(!h)return;m(),typeof n.showPopover=="function"&&n.showPopover(),i.hasAttribute("title")&&(s=i.getAttribute("title"),i.removeAttribute("title"));const w=i.getAttribute("aria-describedby");w?d=w:d=null;const p=document.createElement("div");p.className="ln-tooltip",p.textContent=h,i[c+"Uid"]||(u+=1,i[c+"Uid"]="ln-tooltip-"+u),p.id=i[c+"Uid"],n.appendChild(p);const A=p.offsetWidth,J=p.offsetHeight,Q=i.getBoundingClientRect(),V=i.getAttribute(r)||"top",x=X(Q,{width:A,height:J},V,6);p.style.top=x.top+"px",p.style.left=x.left+"px",p.setAttribute("data-ln-tooltip-placement",x.placement),d?i.setAttribute("aria-describedby",d+" "+p.id):i.setAttribute("aria-describedby",p.id),l=p,o=i,b()}function v(){if(!l){y();return}o&&(d!==null?o.setAttribute("aria-describedby",d):o.removeAttribute("aria-describedby"),d=null,s!==null&&o.setAttribute("title",s)),s=null,l.parentNode&&l.parentNode.removeChild(l),l=null,o=null,n&&typeof n.hidePopover=="function"&&n.matches(":popover-open")&&n.hidePopover(),y()}function C(i){return this.dom=i,i.hasAttribute("data-ln-tooltip-enhanced")||(i.setAttribute("data-ln-tooltip-enhanced",""),this._addedEnhancedAttr=!0),this._onEnter=function(){g(i)},this._onLeave=function(){o===i&&!i.contains(document.activeElement)&&v()},this._onFocus=function(){g(i)},this._onBlur=function(){o===i&&!i.matches(":hover")&&v()},i.addEventListener("mouseenter",this._onEnter),i.addEventListener("mouseleave",this._onLeave),i.addEventListener("focus",this._onFocus,!0),i.addEventListener("blur",this._onBlur,!0),this}C.prototype.destroy=function(){const i=this.dom;i.removeEventListener("mouseenter",this._onEnter),i.removeEventListener("mouseleave",this._onLeave),i.removeEventListener("focus",this._onFocus,!0),i.removeEventListener("blur",this._onBlur,!0),o===i&&v(),this._addedEnhancedAttr&&i.removeAttribute("data-ln-tooltip-enhanced"),delete i[c],delete i[c+"Uid"],I(i,"ln-tooltip:destroyed",{trigger:i})},M("["+t+"], [data-ln-tooltip-enhanced], ["+e+"][title]",c,C,"ln-tooltip")})()})();
@@ -14,6 +14,7 @@ import { computePlacement, dispatch, registerComponent } from '../../ln-core';
14
14
  let activeTooltipNode = null;
15
15
  let activeTrigger = null;
16
16
  let activeStashedTitle = null;
17
+ let activeStashedDescribedBy = null;
17
18
  let escListener = null;
18
19
 
19
20
  function _ensurePortal() {
@@ -24,6 +25,13 @@ import { computePlacement, dispatch, registerComponent } from '../../ln-core';
24
25
  portal.id = PORTAL_ID;
25
26
  document.body.appendChild(portal);
26
27
  }
28
+ // Popover API gives top-layer promotion for free — no ancestor
29
+ // overflow/z-index/transform can clip or bury the portal. `manual`
30
+ // keeps show/hide entirely under this file's control (no native
31
+ // light-dismiss), same convention as ln-dropdown/ln-popover.
32
+ if (!portal.hasAttribute('popover')) {
33
+ portal.setAttribute('popover', 'manual');
34
+ }
27
35
  return portal;
28
36
  }
29
37
 
@@ -52,6 +60,15 @@ import { computePlacement, dispatch, registerComponent } from '../../ln-core';
52
60
 
53
61
  _ensurePortal();
54
62
 
63
+ // Promote before measuring: a manual popover is `display: none` (UA
64
+ // default) until shown, and a hidden ancestor collapses the whole
65
+ // subtree's layout box regardless of the child's own display —
66
+ // so offsetWidth/offsetHeight below would read 0 if this ran later.
67
+ // _hide() above already guarantees the portal is closed, so no
68
+ // InvalidStateError risk here (matches ln-dropdown/ln-popover: bare
69
+ // feature-detect on show, guarded feature-detect on hide).
70
+ if (typeof portal.showPopover === 'function') portal.showPopover();
71
+
55
72
  // Stash + strip `title` while our tooltip is visible so the browser's
56
73
  // native title tooltip does not appear alongside it. Restored on hide.
57
74
  if (trigger.hasAttribute('title')) {
@@ -59,6 +76,14 @@ import { computePlacement, dispatch, registerComponent } from '../../ln-core';
59
76
  trigger.removeAttribute('title');
60
77
  }
61
78
 
79
+ // Stash pre-existing aria-describedby
80
+ const existingDescribedBy = trigger.getAttribute('aria-describedby');
81
+ if (existingDescribedBy) {
82
+ activeStashedDescribedBy = existingDescribedBy;
83
+ } else {
84
+ activeStashedDescribedBy = null;
85
+ }
86
+
62
87
  const node = document.createElement('div');
63
88
  node.className = 'ln-tooltip';
64
89
  node.textContent = text;
@@ -85,7 +110,11 @@ import { computePlacement, dispatch, registerComponent } from '../../ln-core';
85
110
  node.style.left = placement.left + 'px';
86
111
  node.setAttribute('data-ln-tooltip-placement', placement.placement);
87
112
 
88
- trigger.setAttribute('aria-describedby', node.id);
113
+ if (activeStashedDescribedBy) {
114
+ trigger.setAttribute('aria-describedby', activeStashedDescribedBy + ' ' + node.id);
115
+ } else {
116
+ trigger.setAttribute('aria-describedby', node.id);
117
+ }
89
118
 
90
119
  activeTooltipNode = node;
91
120
  activeTrigger = trigger;
@@ -98,7 +127,13 @@ import { computePlacement, dispatch, registerComponent } from '../../ln-core';
98
127
  return;
99
128
  }
100
129
  if (activeTrigger) {
101
- activeTrigger.removeAttribute('aria-describedby');
130
+ if (activeStashedDescribedBy !== null) {
131
+ activeTrigger.setAttribute('aria-describedby', activeStashedDescribedBy);
132
+ } else {
133
+ activeTrigger.removeAttribute('aria-describedby');
134
+ }
135
+ activeStashedDescribedBy = null;
136
+
102
137
  if (activeStashedTitle !== null) {
103
138
  activeTrigger.setAttribute('title', activeStashedTitle);
104
139
  }
@@ -109,6 +144,11 @@ import { computePlacement, dispatch, registerComponent } from '../../ln-core';
109
144
  }
110
145
  activeTooltipNode = null;
111
146
  activeTrigger = null;
147
+ // Exit the top layer — guard required: hidePopover() throws
148
+ // InvalidStateError if called while not currently showing.
149
+ if (portal && typeof portal.hidePopover === 'function' && portal.matches(':popover-open')) {
150
+ portal.hidePopover();
151
+ }
112
152
  _removeEscListener();
113
153
  }
114
154
 
@@ -124,14 +164,13 @@ import { computePlacement, dispatch, registerComponent } from '../../ln-core';
124
164
  this._addedEnhancedAttr = true;
125
165
  }
126
166
 
127
- const self = this;
128
167
  this._onEnter = function () { _show(el); };
129
168
  this._onLeave = function () {
130
- if (activeTrigger === el) _hide();
169
+ if (activeTrigger === el && !el.contains(document.activeElement)) _hide();
131
170
  };
132
171
  this._onFocus = function () { _show(el); };
133
172
  this._onBlur = function () {
134
- if (activeTrigger === el) _hide();
173
+ if (activeTrigger === el && !el.matches(':hover')) _hide();
135
174
  };
136
175
 
137
176
  el.addEventListener('mouseenter', this._onEnter);
@@ -160,7 +199,7 @@ import { computePlacement, dispatch, registerComponent } from '../../ln-core';
160
199
  // ─── Registration ──────────────────────────────────────────
161
200
 
162
201
  registerComponent(
163
- '[' + TRIGGER_SELECTOR + '], [' + TEXT_ATTR + '][title]',
202
+ '[' + TRIGGER_SELECTOR + '], [data-ln-tooltip-enhanced], [' + TEXT_ATTR + '][title]',
164
203
  DOM_ATTRIBUTE,
165
204
  _component,
166
205
  'ln-tooltip'