@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,196 @@
1
+ import { dispatchCancelable, guardBody } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-link';
5
+ const DOM_ATTRIBUTE = 'lnLink';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ // ─── Status Bar ────────────────────────────────────────────
10
+
11
+ let _statusEl = null;
12
+
13
+ function _createStatusBar() {
14
+ _statusEl = document.createElement('div');
15
+ _statusEl.className = 'ln-link-status';
16
+ document.body.appendChild(_statusEl);
17
+ }
18
+
19
+ function _showStatus(url) {
20
+ if (!_statusEl) return;
21
+ _statusEl.textContent = url;
22
+ _statusEl.classList.add('ln-link-status--visible');
23
+ }
24
+
25
+ function _hideStatus() {
26
+ if (!_statusEl) return;
27
+ _statusEl.classList.remove('ln-link-status--visible');
28
+ }
29
+
30
+ // ─── Click Handler ─────────────────────────────────────────
31
+
32
+ function _handleClick(row, e) {
33
+ if (e.target.closest('a, button, input, select, textarea')) return;
34
+
35
+ const link = row.querySelector('a');
36
+ if (!link) return;
37
+
38
+ const href = link.getAttribute('href');
39
+ if (!href) return;
40
+
41
+ if (e.ctrlKey || e.metaKey || e.button === 1) {
42
+ window.open(href, '_blank');
43
+ return;
44
+ }
45
+
46
+ const before = dispatchCancelable(row, 'ln-link:navigate', { target: row, href: href, link: link });
47
+ if (before.defaultPrevented) return;
48
+ link.click();
49
+ }
50
+
51
+ // ─── Hover Handlers ────────────────────────────────────────
52
+
53
+ function _handleMouseEnter(row) {
54
+ const link = row.querySelector('a');
55
+ if (!link) return;
56
+
57
+ const href = link.getAttribute('href');
58
+ if (href) _showStatus(href);
59
+ }
60
+
61
+ function _handleMouseLeave() {
62
+ _hideStatus();
63
+ }
64
+
65
+ // ─── Row Initialization ────────────────────────────────────
66
+
67
+ function _initRow(row) {
68
+ if (row[DOM_ATTRIBUTE + 'Row']) return;
69
+ row[DOM_ATTRIBUTE + 'Row'] = true;
70
+
71
+ if (!row.querySelector('a')) return;
72
+
73
+ row._lnLinkClick = function (e) {
74
+ _handleClick(row, e);
75
+ };
76
+ row._lnLinkEnter = function () {
77
+ _handleMouseEnter(row);
78
+ };
79
+
80
+ row.addEventListener('click', row._lnLinkClick);
81
+ row.addEventListener('mouseenter', row._lnLinkEnter);
82
+ row.addEventListener('mouseleave', _handleMouseLeave);
83
+ }
84
+
85
+ function _destroyRow(row) {
86
+ if (!row[DOM_ATTRIBUTE + 'Row']) return;
87
+ if (row._lnLinkClick) row.removeEventListener('click', row._lnLinkClick);
88
+ if (row._lnLinkEnter) row.removeEventListener('mouseenter', row._lnLinkEnter);
89
+ row.removeEventListener('mouseleave', _handleMouseLeave);
90
+ delete row._lnLinkClick;
91
+ delete row._lnLinkEnter;
92
+ delete row[DOM_ATTRIBUTE + 'Row'];
93
+ }
94
+
95
+ function _destroyContainer(container) {
96
+ if (!container[DOM_ATTRIBUTE + 'Init']) return;
97
+ const tag = container.tagName;
98
+
99
+ if (tag === 'TABLE' || tag === 'TBODY') {
100
+ const tbody = (tag === 'TABLE') ? container.querySelector('tbody') || container : container;
101
+ for (const row of tbody.querySelectorAll('tr')) {
102
+ _destroyRow(row);
103
+ }
104
+ } else {
105
+ _destroyRow(container);
106
+ }
107
+
108
+ delete container[DOM_ATTRIBUTE + 'Init'];
109
+ }
110
+
111
+ // ─── Container Initialization ──────────────────────────────
112
+
113
+ function _initContainer(container) {
114
+ if (container[DOM_ATTRIBUTE + 'Init']) return;
115
+ container[DOM_ATTRIBUTE + 'Init'] = true;
116
+
117
+ const tag = container.tagName;
118
+
119
+ if (tag === 'TABLE' || tag === 'TBODY') {
120
+ const tbody = (tag === 'TABLE') ? container.querySelector('tbody') || container : container;
121
+ for (const row of tbody.querySelectorAll('tr')) {
122
+ _initRow(row);
123
+ }
124
+ } else {
125
+ _initRow(container);
126
+ }
127
+ }
128
+
129
+ // ─── Find & Init ───────────────────────────────────────────
130
+
131
+ // Local findElements — intentional divergence from ln-core helper: invokes _initContainer (function, flag-based) and processes child rows.
132
+ function findElements(root) {
133
+ if (root.hasAttribute && root.hasAttribute(DOM_SELECTOR)) {
134
+ _initContainer(root);
135
+ }
136
+
137
+ const containers = root.querySelectorAll ? root.querySelectorAll('[' + DOM_SELECTOR + ']') : [];
138
+ for (const c of containers) {
139
+ _initContainer(c);
140
+ }
141
+ }
142
+
143
+ // ─── DOM Observer ──────────────────────────────────────────
144
+
145
+ function _domObserver() {
146
+ guardBody(function () {
147
+ const observer = new MutationObserver(function (mutations) {
148
+ for (const mutation of mutations) {
149
+ if (mutation.type === 'childList') {
150
+ for (const node of mutation.addedNodes) {
151
+ if (node.nodeType === 1) {
152
+ findElements(node);
153
+
154
+ if (node.tagName === 'TR') {
155
+ const parent = node.closest('[' + DOM_SELECTOR + ']');
156
+ if (parent) _initRow(node);
157
+ }
158
+ }
159
+ }
160
+ } else if (mutation.type === 'attributes') {
161
+ findElements(mutation.target);
162
+ }
163
+ }
164
+ });
165
+
166
+ observer.observe(document.body, {
167
+ childList: true,
168
+ subtree: true,
169
+ attributes: true,
170
+ attributeFilter: [DOM_SELECTOR]
171
+ });
172
+ }, 'ln-link');
173
+ }
174
+
175
+ // ─── Constructor (public API) ──────────────────────────────
176
+
177
+ function constructor(domRoot) {
178
+ findElements(domRoot);
179
+ }
180
+
181
+ // ─── Init ──────────────────────────────────────────────────
182
+
183
+ window[DOM_ATTRIBUTE] = { init: constructor, destroy: _destroyContainer };
184
+
185
+ function _initializeAll() {
186
+ _createStatusBar();
187
+ _domObserver();
188
+ constructor(document.body);
189
+ }
190
+
191
+ if (document.readyState === 'loading') {
192
+ document.addEventListener('DOMContentLoaded', _initializeAll);
193
+ } else {
194
+ _initializeAll();
195
+ }
196
+ })();
@@ -0,0 +1,154 @@
1
+ # ln-modal
2
+
3
+ > Focus-gated viewport-blocking dialog overlays, managed reactively via the DOM.
4
+
5
+ ---
6
+
7
+ ## 1. Philosophy & The Modal Mindset
8
+
9
+ In `ln-ashlar`, the core design principle is **orthogonality**. Rather than creating a heavy component that bundles form handlers, visual layouts, focus traps, backdrop styling, and dimensions, `ln-modal` splits them cleanly:
10
+
11
+ 1. **State & Accessibility (JavaScript)**: The `ln-modal` script (145 lines) only manages binary `open` / `close` state, suppresses parent `<body>` scrolling, intercepts tab navigation to trap focus inside the modal, closes topmost modals on ESC key, and restores focus back to the trigger on close.
12
+ 2. **The Content Root (HTML)**: The modal content root is ALWAYS a `<form>`. The form IS the panel — there are no redundant BEM wrappers like `.ln-modal__content`. Cancel and submit buttons live directly inside the form.
13
+ 3. **Visual Presentation & Sizing (CSS)**: Overlay backdrops, Sticky headers/footers, and scrollable body areas are styled using Vanilla CSS. Sizing variants (`modal-sm|md|lg|xl`) are applied via SCSS mixins on `> form`, keeping markup completely clean.
14
+
15
+ ---
16
+
17
+ ## 2. Minimal Blueprint
18
+
19
+ Triggers and modals are paired by ID. The overlay has `class="ln-modal"` and `data-ln-modal`. The direct child must always be a `<form>`.
20
+
21
+ ```html
22
+ <!-- Trigger button -->
23
+ <button data-ln-modal-for="user-modal">Add User</button>
24
+
25
+ <!-- Modal overlay -->
26
+ <div class="ln-modal" data-ln-modal id="user-modal">
27
+ <form>
28
+ <!-- Header -->
29
+ <header>
30
+ <h3>Add User</h3>
31
+ <button type="button" data-ln-modal-close aria-label="Close">
32
+ <svg class="ln-icon" aria-hidden="true"><use href="#ln-x"></use></svg>
33
+ </button>
34
+ </header>
35
+
36
+ <!-- Scrollable content -->
37
+ <main>
38
+ <label>Name <input type="text" name="name" autofocus></label>
39
+ </main>
40
+
41
+ <!-- Sticky footer -->
42
+ <footer>
43
+ <button type="button" data-ln-modal-close>Cancel</button>
44
+ <button type="submit">Save</button>
45
+ </footer>
46
+ </form>
47
+ </div>
48
+ ```
49
+
50
+ ### Key Anatomy Rules
51
+ - **The Overlay (`data-ln-modal`)**: Driven by the value `"open"` (open) and `"close"` or empty (closed).
52
+ - **The Trigger (`data-ln-modal-for="id"`)**: Placed on buttons/links to toggle modal display.
53
+ - **The Dismiss button (`data-ln-modal-close`)**: Placed on cancel or close buttons. Always needs `type="button"` inside a form.
54
+ - **Focus Override (`autofocus`)**: Place on any form field to override the default behavior of focusing the first input on open.
55
+
56
+ ---
57
+
58
+ ## 3. Declarative API & State Contract
59
+
60
+ There are no imperative JavaScript methods (like `open()` or `close()`) on the component instance. **The HTML attribute is the sole contract.**
61
+
62
+ Triggers, backdrop dismissals, ESC handlers, and custom scripts all change state by writing the active attribute on the modal element:
63
+
64
+ ```js
65
+ const modal = document.getElementById('user-modal');
66
+
67
+ // Open the modal
68
+ modal.setAttribute('data-ln-modal', 'open');
69
+
70
+ // Close the modal
71
+ modal.setAttribute('data-ln-modal', 'close');
72
+
73
+ // Read-only state query
74
+ modal.lnModal.isOpen; // Returns true/false
75
+ ```
76
+
77
+ ### Attributes
78
+ - `data-ln-modal`: Placed on the overlay element. Value `"open"` = open; `"close"` = closed.
79
+ - `data-ln-modal-for="id"`: Placed on trigger elements referencing the modal ID.
80
+ - `data-ln-modal-close`: Placed on any close trigger inside the modal.
81
+
82
+ ---
83
+
84
+ ## 4. Transition Events
85
+
86
+ All events bubble. The dispatch target is the overlay element. Every event's `detail` carries `{ modalId, target }`.
87
+
88
+ | Event | Cancelable | Dispatched When |
89
+ |---|:---:|---|
90
+ | **`ln-modal:before-open`** | **Yes** | Before opening. Calling `event.preventDefault()` cancels the transition and reverts the attribute. |
91
+ | **`ln-modal:open`** | No | After modal is active, body scroll locked, and focus trapped. |
92
+ | **`ln-modal:before-close`** | **Yes** | Before closing. Calling `event.preventDefault()` cancels the close and reverts the attribute. |
93
+ | **`ln-modal:close`** | No | After modal is closed, scroll locks released, and focus restored. |
94
+
95
+ ```js
96
+ // Example: Block close transition if form is dirty (unsaved changes)
97
+ const modal = document.getElementById('user-modal');
98
+ modal.addEventListener('ln-modal:before-close', (e) => {
99
+ if (formIsDirty()) {
100
+ e.preventDefault(); // Reverts attribute back to "open"
101
+ }
102
+ });
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 5. Visual Sizing & SCSS Mixins
108
+
109
+ Do not use visual layout utility classes in your markup. Apply structural sizing variants inside your SCSS to the modal form element:
110
+
111
+ | Mixin | `max-width` | Ideal For |
112
+ |---|---|---|
113
+ | `@include modal-sm;` | `28rem` | Simple confirms, single-field inputs |
114
+ | `@include modal-md;` | `32rem` | Standard 2-4 field forms (Default) |
115
+ | `@include modal-lg;` | `42rem` | Wide multi-column forms, data lists |
116
+ | `@include modal-xl;` | `48rem` | Detail previews, large forms |
117
+
118
+ ```scss
119
+ // Apply in project SCSS
120
+ .ln-modal {
121
+ @include modal-overlay;
122
+
123
+ > form {
124
+ @include modal-panel;
125
+ @include modal-md; // default
126
+ }
127
+
128
+ &#user-modal > form {
129
+ @include modal-lg; // Override specific modal width
130
+ }
131
+ }
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 6. Integration & Source Files
137
+
138
+ - **Unified Bundle**: Loaded automatically with the main bundle:
139
+ ```html
140
+ <script src="dist/ln-ashlar.iife.js" defer></script>
141
+ ```
142
+ - **Standalone IIFE**: For lightweight pages, load the standalone, self-registering IIFE version:
143
+ ```html
144
+ <script src="js/ln-modal/ln-modal.js" defer></script>
145
+ ```
146
+ - **Active Source (ESM)**: Development source is located at [js/ln-modal/src/ln-modal.js](file:///c:/laragon/www/ln-ashlar/js/ln-modal/src/ln-modal.js).
147
+
148
+ ---
149
+
150
+ ## Related
151
+ - **[`ln-confirm`](../ln-confirm/README.md)** — Two-click inline confirm actions (lightweight alternative to modals).
152
+ - **[`ln-form`](../ln-form/README.md)** — Form serialization and success/error cascades.
153
+ - **[`ln-validate`](../ln-validate/README.md)** — Declarative field-level constraints and visual errors.
154
+ - **Architecture deep-dive** — [`docs/js/modal.md`](../../docs/js/modal.md).
@@ -0,0 +1 @@
1
+ (function(){"use strict";function h(n,e,c){n.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:c||{}}))}function v(n,e,c){const l=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:c||{}});return n.dispatchEvent(l),l}function E(n,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){E(n,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}n()}function g(n,e,c,l){if(n.nodeType!==1)return;const m=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",t=Array.from(n.querySelectorAll(m));n.matches&&n.matches(m)&&t.push(n);for(const o of t)o[c]||(o[c]=new l(o))}function y(n){return!!(n.offsetWidth||n.offsetHeight||n.getClientRects().length)}function _(n,e,c,l,f={}){const m=f.extraAttributes||[],t=f.onAttributeChange||null,o=f.onInit||null;function r(d){const i=d||document.body;g(i,n,e,c),o&&o(i)}return E(function(){const d=new MutationObserver(function(u){for(let s=0;s<u.length;s++){const a=u[s];if(a.type==="childList")for(let b=0;b<a.addedNodes.length;b++){const p=a.addedNodes[b];p.nodeType===1&&(g(p,n,e,c),o&&o(p))}else a.type==="attributes"&&(t&&a.target[e]?t(a.target,a.attributeName):(g(a.target,n,e,c),o&&o(a.target)))}});let i=[];if(n.indexOf("[")!==-1){const u=/\[([\w-]+)/g;let s;for(;(s=u.exec(n))!==null;)i.push(s[1])}else i.push(n);d.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:i.concat(m)})},l),window[e]=r,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){r(document.body)}):r(document.body),r}const A={};function C(n,e){A[n]=e}function L(n){return A[n]||{ingress:e=>e,egress:e=>e}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=C,window.lnCore.getDataMapper=L),(function(){const n="data-ln-modal",e="lnModal";if(window[e]!==void 0)return;function c(t){const o=Array.from(t.querySelectorAll("[data-ln-modal-for]"));t.hasAttribute&&t.hasAttribute("data-ln-modal-for")&&o.push(t);for(const r of o){if(r[e+"Trigger"])continue;const d=function(i){if(i.ctrlKey||i.metaKey||i.button===1)return;i.preventDefault();const u=r.getAttribute("data-ln-modal-for"),s=document.getElementById(u);if(!s){console.warn('[ln-modal] No modal found for data-ln-modal-for="'+u+'"');return}if(!s[e])return;const a=s.getAttribute(n);s.setAttribute(n,a==="open"?"close":"open")};r.addEventListener("click",d),r[e+"Trigger"]=d}}function l(t){this.dom=t,this.isOpen=t.getAttribute(n)==="open";const o=this;return this._onEscape=function(r){r.key==="Escape"&&o.dom.setAttribute(n,"close")},this._onFocusTrap=function(r){if(r.key!=="Tab")return;const d=Array.prototype.filter.call(o.dom.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'),y);if(d.length===0)return;const i=d[0],u=d[d.length-1];r.shiftKey?document.activeElement===i&&(r.preventDefault(),u.focus()):document.activeElement===u&&(r.preventDefault(),i.focus())},this._onClose=function(r){r.preventDefault(),o.dom.setAttribute(n,"close")},m(this),this.isOpen&&(this.dom.setAttribute("aria-modal","true"),this.dom.setAttribute("role","dialog"),document.body.classList.add("ln-modal-open"),document.addEventListener("keydown",this._onEscape),document.addEventListener("keydown",this._onFocusTrap)),this}l.prototype.destroy=function(){if(!this.dom[e])return;this.isOpen&&(this.dom.removeAttribute("aria-modal"),document.removeEventListener("keydown",this._onEscape),document.removeEventListener("keydown",this._onFocusTrap),this._returnFocusEl=null,document.querySelector("["+n+'="open"]')||document.body.classList.remove("ln-modal-open"));const t=this.dom.querySelectorAll("[data-ln-modal-close]");for(const r of t)r[e+"Close"]&&(r.removeEventListener("click",r[e+"Close"]),delete r[e+"Close"]);const o=document.querySelectorAll('[data-ln-modal-for="'+this.dom.id+'"]');for(const r of o)r[e+"Trigger"]&&(r.removeEventListener("click",r[e+"Trigger"]),delete r[e+"Trigger"]);h(this.dom,"ln-modal:destroyed",{modalId:this.dom.id,target:this.dom}),delete this.dom[e]};function f(t){const o=t[e];if(!o)return;const d=t.getAttribute(n)==="open";if(d!==o.isOpen)if(d){if(v(t,"ln-modal:before-open",{modalId:t.id,target:t}).defaultPrevented){t.setAttribute(n,"close");return}o.isOpen=!0,t.setAttribute("aria-modal","true"),t.setAttribute("role","dialog"),document.body.classList.add("ln-modal-open"),document.addEventListener("keydown",o._onEscape),document.addEventListener("keydown",o._onFocusTrap);const u=document.activeElement;o._returnFocusEl=u&&u!==document.body?u:null;const s=t.querySelector("[autofocus]");if(s&&y(s))s.focus();else{const a=t.querySelectorAll('input:not([disabled]):not([type="hidden"]), textarea:not([disabled]), select:not([disabled])'),b=Array.prototype.find.call(a,y);if(b)b.focus();else{const p=t.querySelectorAll("a[href], button:not([disabled])"),w=Array.prototype.find.call(p,y);w&&w.focus()}}h(t,"ln-modal:open",{modalId:t.id,target:t})}else{if(v(t,"ln-modal:before-close",{modalId:t.id,target:t}).defaultPrevented){t.setAttribute(n,"open");return}o.isOpen=!1,t.removeAttribute("aria-modal"),document.removeEventListener("keydown",o._onEscape),document.removeEventListener("keydown",o._onFocusTrap),h(t,"ln-modal:close",{modalId:t.id,target:t}),o._returnFocusEl&&document.contains(o._returnFocusEl)&&typeof o._returnFocusEl.focus=="function"&&o._returnFocusEl.focus(),o._returnFocusEl=null,document.querySelector("["+n+'="open"]')||document.body.classList.remove("ln-modal-open")}}function m(t){const o=t.dom.querySelectorAll("[data-ln-modal-close]");for(const r of o)r[e+"Close"]||(r.addEventListener("click",t._onClose),r[e+"Close"]=t._onClose)}_(n,e,l,"ln-modal",{extraAttributes:["data-ln-modal-for"],onAttributeChange:f,onInit:c})})()})();
@@ -0,0 +1,11 @@
1
+ // ── JS state: modal open/close ──
2
+ // data-ln-modal attribute drives visibility. JS sets "open"/"close".
3
+ [data-ln-modal="open"] {
4
+ display: flex;
5
+ opacity: 1;
6
+ }
7
+
8
+ // Prevent body scroll when modal is open
9
+ body.ln-modal-open {
10
+ overflow: hidden;
11
+ }
@@ -0,0 +1,201 @@
1
+ import { registerComponent, dispatch, dispatchCancelable, isVisible } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-modal';
5
+ const DOM_ATTRIBUTE = 'lnModal';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+
10
+ function _attachTriggers(root) {
11
+ const triggers = Array.from(root.querySelectorAll('[data-ln-modal-for]'));
12
+ if (root.hasAttribute && root.hasAttribute('data-ln-modal-for')) {
13
+ triggers.push(root);
14
+ }
15
+ for (const btn of triggers) {
16
+ if (btn[DOM_ATTRIBUTE + 'Trigger']) continue;
17
+ const handler = function (e) {
18
+ if (e.ctrlKey || e.metaKey || e.button === 1) return;
19
+ e.preventDefault();
20
+ const modalId = btn.getAttribute('data-ln-modal-for');
21
+ const target = document.getElementById(modalId);
22
+ if (!target) {
23
+ console.warn('[ln-modal] No modal found for data-ln-modal-for="' + modalId + '"');
24
+ return;
25
+ }
26
+ if (!target[DOM_ATTRIBUTE]) return;
27
+ const current = target.getAttribute(DOM_SELECTOR);
28
+ target.setAttribute(DOM_SELECTOR, current === 'open' ? 'close' : 'open');
29
+ };
30
+ btn.addEventListener('click', handler);
31
+ btn[DOM_ATTRIBUTE + 'Trigger'] = handler;
32
+ }
33
+ }
34
+
35
+ // ─── Component ─────────────────────────────────────────────
36
+
37
+ function _component(dom) {
38
+ this.dom = dom;
39
+ this.isOpen = dom.getAttribute(DOM_SELECTOR) === 'open';
40
+
41
+ const self = this;
42
+
43
+ this._onEscape = function (e) {
44
+ if (e.key === 'Escape') self.dom.setAttribute(DOM_SELECTOR, 'close');
45
+ };
46
+
47
+ this._onFocusTrap = function (e) {
48
+ if (e.key !== 'Tab') return;
49
+ // Focuses in DOM order — positive tabindex not supported (anti-pattern per WCAG)
50
+ const focusable = Array.prototype.filter.call(
51
+ self.dom.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'),
52
+ isVisible
53
+ );
54
+ if (focusable.length === 0) return;
55
+ const first = focusable[0];
56
+ const last = focusable[focusable.length - 1];
57
+ if (e.shiftKey) {
58
+ if (document.activeElement === first) { e.preventDefault(); last.focus(); }
59
+ } else {
60
+ if (document.activeElement === last) { e.preventDefault(); first.focus(); }
61
+ }
62
+ };
63
+
64
+ this._onClose = function (e) {
65
+ e.preventDefault();
66
+ self.dom.setAttribute(DOM_SELECTOR, 'close');
67
+ };
68
+
69
+ _attachCloseButtons(this);
70
+
71
+ // Apply initial state if open
72
+ if (this.isOpen) {
73
+ this.dom.setAttribute('aria-modal', 'true');
74
+ this.dom.setAttribute('role', 'dialog');
75
+ document.body.classList.add('ln-modal-open');
76
+ document.addEventListener('keydown', this._onEscape);
77
+ document.addEventListener('keydown', this._onFocusTrap);
78
+ }
79
+
80
+ return this;
81
+ }
82
+
83
+ _component.prototype.destroy = function () {
84
+ if (!this.dom[DOM_ATTRIBUTE]) return;
85
+
86
+ if (this.isOpen) {
87
+ this.dom.removeAttribute('aria-modal');
88
+ document.removeEventListener('keydown', this._onEscape);
89
+ document.removeEventListener('keydown', this._onFocusTrap);
90
+ this._returnFocusEl = null;
91
+ if (!document.querySelector('[' + DOM_SELECTOR + '="open"]')) {
92
+ document.body.classList.remove('ln-modal-open');
93
+ }
94
+ }
95
+
96
+ const closeButtons = this.dom.querySelectorAll('[data-ln-modal-close]');
97
+ for (const btn of closeButtons) {
98
+ if (btn[DOM_ATTRIBUTE + 'Close']) {
99
+ btn.removeEventListener('click', btn[DOM_ATTRIBUTE + 'Close']);
100
+ delete btn[DOM_ATTRIBUTE + 'Close'];
101
+ }
102
+ }
103
+
104
+ const triggers = document.querySelectorAll('[data-ln-modal-for="' + this.dom.id + '"]');
105
+ for (const btn of triggers) {
106
+ if (btn[DOM_ATTRIBUTE + 'Trigger']) {
107
+ btn.removeEventListener('click', btn[DOM_ATTRIBUTE + 'Trigger']);
108
+ delete btn[DOM_ATTRIBUTE + 'Trigger'];
109
+ }
110
+ }
111
+
112
+ dispatch(this.dom, 'ln-modal:destroyed', { modalId: this.dom.id, target: this.dom });
113
+ delete this.dom[DOM_ATTRIBUTE];
114
+ };
115
+
116
+ // ─── Attribute Sync ────────────────────────────────────────
117
+
118
+ function _syncAttribute(el) {
119
+ const instance = el[DOM_ATTRIBUTE];
120
+ if (!instance) return;
121
+
122
+ const value = el.getAttribute(DOM_SELECTOR);
123
+ const shouldBeOpen = value === 'open';
124
+
125
+ if (shouldBeOpen === instance.isOpen) return;
126
+
127
+ if (shouldBeOpen) {
128
+ const before = dispatchCancelable(el, 'ln-modal:before-open', { modalId: el.id, target: el });
129
+ if (before.defaultPrevented) {
130
+ el.setAttribute(DOM_SELECTOR, 'close');
131
+ return;
132
+ }
133
+ instance.isOpen = true;
134
+ el.setAttribute('aria-modal', 'true');
135
+ el.setAttribute('role', 'dialog');
136
+ document.body.classList.add('ln-modal-open');
137
+ document.addEventListener('keydown', instance._onEscape);
138
+ document.addEventListener('keydown', instance._onFocusTrap);
139
+
140
+ const previouslyFocused = document.activeElement;
141
+ instance._returnFocusEl = (previouslyFocused && previouslyFocused !== document.body) ? previouslyFocused : null;
142
+
143
+ const autoFocusEl = el.querySelector('[autofocus]');
144
+ if (autoFocusEl && isVisible(autoFocusEl)) {
145
+ autoFocusEl.focus();
146
+ } else {
147
+ const inputs = el.querySelectorAll('input:not([disabled]):not([type="hidden"]), textarea:not([disabled]), select:not([disabled])');
148
+ const firstInput = Array.prototype.find.call(inputs, isVisible);
149
+ if (firstInput) firstInput.focus();
150
+ else {
151
+ const buttons = el.querySelectorAll('a[href], button:not([disabled])');
152
+ const firstFocusable = Array.prototype.find.call(buttons, isVisible);
153
+ if (firstFocusable) firstFocusable.focus();
154
+ }
155
+ }
156
+
157
+ dispatch(el, 'ln-modal:open', { modalId: el.id, target: el });
158
+ } else {
159
+ const before = dispatchCancelable(el, 'ln-modal:before-close', { modalId: el.id, target: el });
160
+ if (before.defaultPrevented) {
161
+ el.setAttribute(DOM_SELECTOR, 'open');
162
+ return;
163
+ }
164
+ instance.isOpen = false;
165
+ el.removeAttribute('aria-modal');
166
+ document.removeEventListener('keydown', instance._onEscape);
167
+ document.removeEventListener('keydown', instance._onFocusTrap);
168
+ dispatch(el, 'ln-modal:close', { modalId: el.id, target: el });
169
+
170
+ if (instance._returnFocusEl
171
+ && document.contains(instance._returnFocusEl)
172
+ && typeof instance._returnFocusEl.focus === 'function') {
173
+ instance._returnFocusEl.focus();
174
+ }
175
+ instance._returnFocusEl = null;
176
+
177
+ if (!document.querySelector('[' + DOM_SELECTOR + '="open"]')) {
178
+ document.body.classList.remove('ln-modal-open');
179
+ }
180
+ }
181
+ }
182
+
183
+ // ─── Helpers ───────────────────────────────────────────────
184
+
185
+ function _attachCloseButtons(instance) {
186
+ const closeButtons = instance.dom.querySelectorAll('[data-ln-modal-close]');
187
+ for (const btn of closeButtons) {
188
+ if (btn[DOM_ATTRIBUTE + 'Close']) continue;
189
+ btn.addEventListener('click', instance._onClose);
190
+ btn[DOM_ATTRIBUTE + 'Close'] = instance._onClose;
191
+ }
192
+ }
193
+
194
+ // ─── Init ──────────────────────────────────────────────────
195
+
196
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-modal', {
197
+ extraAttributes: ['data-ln-modal-for'],
198
+ onAttributeChange: _syncAttribute,
199
+ onInit: _attachTriggers
200
+ });
201
+ })();
@@ -0,0 +1,70 @@
1
+ # ln-nav
2
+
3
+ Active link highlighter — automatically marks the active link in navigation based on the current URL.
4
+ Works with `pushState` (ln-ajax) and `popstate` (browser back/forward).
5
+
6
+ For internal mechanics — singleton `history.pushState` patch, per-instance MutationObserver, URL normalization — see [`docs/js/nav.md`](../../docs/js/nav.md).
7
+
8
+ ## Integration
9
+
10
+ ### In-Bundle (Standard Integration)
11
+ To load `ln-nav` as part of the unified `ln-ashlar` bundle, include the main script:
12
+ ```html
13
+ <script src="dist/ln-ashlar.iife.js" defer></script>
14
+ ```
15
+
16
+ ### Standalone (Zero-Dependency IIFE)
17
+ If you only need the active link highlighter component, load the compiled zero-dependency IIFE directly:
18
+ ```html
19
+ <script src="js/ln-nav/ln-nav.js" defer></script>
20
+ ```
21
+
22
+ ### Source Files & Development
23
+ - **Active Development Source**: [js/ln-nav/src/ln-nav.js](file:///c:/laragon/www/ln-ashlar/js/ln-nav/src/ln-nav.js) — The source of truth for component logic.
24
+ - **Compiled Standalone**: [js/ln-nav/ln-nav.js](file:///c:/laragon/www/ln-ashlar/js/ln-nav/ln-nav.js) — The compiled, ready-to-use standalone bundle.
25
+
26
+ ## Attributes
27
+
28
+ | Attribute | On | Description |
29
+ |-----------|-----|-------------|
30
+ | `data-ln-nav="className"` | `<nav>` element | CSS class to apply to the active link (any class name) |
31
+
32
+ ## Behavior
33
+
34
+ - Compares each `<a href>` inside the nav against `window.location.pathname`.
35
+ - **Match rule**: exact match (`/users` == `/users`) OR parent-prefix match (`/users` matches `/users/42`). Root (`/`) is exact-only — it never matches as a parent. Trailing slashes are normalized (`/users/` == `/users`).
36
+ - Re-runs on `popstate` (browser back/forward) and on `history.pushState` (ln-ajax navigation) automatically — no configuration.
37
+ - New `<a>` elements added under the nav after init are scored immediately (per-instance MutationObserver).
38
+
39
+ ## HTML Structure
40
+
41
+ ```html
42
+ <nav data-ln-nav="active">
43
+ <ul>
44
+ <li><a href="/dashboard">Dashboard</a></li>
45
+ <li><a href="/users">Users</a></li>
46
+ <li><a href="/settings">Settings</a></li>
47
+ </ul>
48
+ </nav>
49
+ ```
50
+
51
+ If the URL is `/users/42`, the `/users` link will get the `active` class.
52
+
53
+ ## CSS
54
+
55
+ ```scss
56
+ nav a {
57
+ --color-fg: var(--fg-muted);
58
+ color: var(--color-fg);
59
+ &.active {
60
+ @include text-primary;
61
+ @include font-bold;
62
+ --color-bg: var(--bg-sunken);
63
+ background: var(--color-bg);
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## API
69
+
70
+ `data-ln-nav` is the contract — setting the attribute on a connected `<nav>` is sufficient (the document-level MutationObserver picks it up). For custom roots that the observer does not watch (Shadow DOM, iframe), call `window.lnNav(rootElement)` to upgrade manually. Each `[data-ln-nav]` element exposes `element.lnNav.destroy()` for teardown.
@@ -0,0 +1 @@
1
+ (function(){"use strict";function y(r,s){if(!document.body){document.addEventListener("DOMContentLoaded",function(){y(r,s)}),console.warn("["+s+'] Script loaded before <body> — add "defer" to your <script> tag');return}r()}const h={};function m(r,s){h[r]=s}function g(r){return h[r]||{ingress:s=>s,egress:s=>s}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=m,window.lnCore.getDataMapper=g),(function(){const r="data-ln-nav",s="lnNav";if(window[s]!==void 0)return;const w=new WeakMap,l=[];if(!history._lnNavPatched){const t=history.pushState;history.pushState=function(){t.apply(history,arguments);for(const o of l)o()},history._lnNavPatched=!0}function f(t){if(!t.hasAttribute(r)||w.has(t))return;const o=t.getAttribute(r);if(!o)return;const e=S(t,o);w.set(t,e),t[s]=e}function S(t,o){let e=Array.from(t.querySelectorAll("a"));p(e,o,window.location.pathname);const n=function(){e=Array.from(t.querySelectorAll("a")),p(e,o,window.location.pathname)};window.addEventListener("popstate",n),l.push(n);const a=new MutationObserver(function(d){for(const u of d)if(u.type==="childList"){for(const i of u.addedNodes)if(i.nodeType===1){if(i.tagName==="A")e.push(i),p([i],o,window.location.pathname);else if(i.querySelectorAll){const c=Array.from(i.querySelectorAll("a"));e=e.concat(c),p(c,o,window.location.pathname)}}for(const i of u.removedNodes)if(i.nodeType===1){if(i.tagName==="A")e=e.filter(function(c){return c!==i});else if(i.querySelectorAll){const c=Array.from(i.querySelectorAll("a"));e=e.filter(function(v){return!c.includes(v)})}}}});return a.observe(t,{childList:!0,subtree:!0}),{navElement:t,activeClass:o,observer:a,updateHandler:n,destroy:function(){a.disconnect(),window.removeEventListener("popstate",n);const d=l.indexOf(n);d!==-1&&l.splice(d,1),w.delete(t),delete t[s]}}}function b(t){try{return new URL(t,window.location.href).pathname.replace(/\/$/,"")||"/"}catch{return t.replace(/\/$/,"")||"/"}}function p(t,o,e){const n=b(e);for(const a of t){const d=a.getAttribute("href");if(!d)continue;const u=b(d);a.classList.remove(o);const i=u===n,c=u!=="/"&&n.startsWith(u+"/");(i||c)&&a.classList.add(o)}}function L(){y(function(){new MutationObserver(function(o){for(const e of o)if(e.type==="childList"){for(const n of e.addedNodes)if(n.nodeType===1&&(n.hasAttribute&&n.hasAttribute(r)&&f(n),n.querySelectorAll))for(const a of n.querySelectorAll("["+r+"]"))f(a)}else e.type==="attributes"&&e.target.hasAttribute&&e.target.hasAttribute(r)&&f(e.target)}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[r]})},"ln-nav")}window[s]=f;function A(){for(const t of document.querySelectorAll("["+r+"]"))f(t)}L(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",A):A()})()})();