@livenetworks/ashlar 1.5.1 → 1.6.1

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 +165 -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 +82 -42
  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
package/js/COMPONENTS.md CHANGED
@@ -169,9 +169,8 @@ _component.prototype.open = function () {
169
169
  While utilizing an `<input type="checkbox">` and the CSS sibling selector (`:checked ~ .menu`) is a common "pure CSS" pattern for toggling dropdowns or modals, it is **off-doctrine** for `ln-ashlar` components and must not be used:
170
170
 
171
171
  1. **Non-Observable Programmatic Updates:** Changing `.checked = true` programmatically via JavaScript **does not** dispatch native browser `'change'` or `'input'` events. It also **does not** trigger the `MutationObserver` (which only watches attribute modifications in the HTML markup). This completely breaks the reactive attribute-driven bridge.
172
- 2. **Teleportation Conflict:** `ln-ashlar` components regularly teleport overlays to `document.body` (e.g., dropdown menus, modals) to prevent parent `z-index` and `overflow: hidden` issues. Teleportation immediately breaks the CSS sibling/child relationships required for CSS-only checkbox selectors.
173
- 3. **Broken Encapsulation:** A checkbox lives inside or beside the component. To read or write state, external coordinators or other components would have to query the internal DOM structure (e.g., `modal.querySelector('input[type="checkbox"]').checked`), exposing private details.
174
- 4. **Accessibility (ARIA) Semantic Mismatch:** Modals, dropdown triggers, and popovers require specific ARIA roles (e.g., `role="dialog"`, `aria-haspopup="menu"`, `aria-expanded`). A checkbox natively announces itself as a checkbox, which confuses screen readers and causes accessibility regressions.
172
+ 2. **Broken Encapsulation:** A checkbox lives inside or beside the component. To read or write state, external coordinators or other components would have to query the internal DOM structure (e.g., `modal.querySelector('input[type="checkbox"]').checked`), exposing private details.
173
+ 3. **Accessibility (ARIA) Semantic Mismatch:** Modals, dropdown triggers, and popovers require specific ARIA roles (e.g., `role="dialog"`, `aria-haspopup="menu"`, `aria-expanded`). A checkbox natively announces itself as a checkbox, which confuses screen readers and causes accessibility regressions.
175
174
 
176
175
  ---
177
176
 
@@ -279,7 +278,7 @@ Use reactive rendering when a component has **2+ state properties** that togethe
279
278
 
280
279
  **Don't use it for:**
281
280
  - Single boolean state — `this.isOpen = true; dom.classList.toggle(...)` is simpler
282
- - Event-driven rendering — components that receive data via `set-data` events (ln-data-table)
281
+ - Event-driven rendering — components that receive data via `set-data` events (ln-table)
283
282
  - One-shot renders — clone template + fill once, no ongoing state
284
283
 
285
284
  ### Used in
@@ -555,7 +554,7 @@ Do NOT standardize to one pattern — each exists for architectural reasons.
555
554
  | DOM instance | `element.ln{Name}` | `el.lnToggle` |
556
555
  | Custom event | `ln-{name}:{action}` | `ln-toggle:open` |
557
556
  | CSS class | `.ln-{name}__{element}` | `.ln-toggle__backdrop` |
558
- | Initialized flag | `data-ln-{name}-initialized` | `data-ln-toggle-for-initialized` |
557
+ | Initialized flag | `element.ln{Name}` | `el.lnToggle` |
559
558
  | Private function | `_functionName` | `_render`, `_attachTriggers` |
560
559
 
561
560
  ---
@@ -625,6 +624,17 @@ nav.dispatchEvent(new CustomEvent('ln-profile:request-create', {
625
624
  | **Command** (changes state) | request event | `nav.dispatchEvent(new CustomEvent('ln-profile:request-remove', { detail: { id } }))` |
626
625
  | **Query** (reads state) | direct access | `nav.lnProfile.currentId`, `sidebar.lnPlaylist.getTrack(idx)` |
627
626
 
627
+ ### Request-Reply / Collector Pattern (Sync Gathering)
628
+
629
+ For commands that require synchronous feedback or list gathering (such as batch validation before submit):
630
+ - **Format:** `ln-{name}:request-{action}` with `e.detail` containing a mutable data structure (like an array).
631
+ - **Example:** `ln-validate:request-validate` with `detail: { invalidFields: [] }`.
632
+ - **Mechanism:** The caller dispatches the event on a common ancestor (like `<form>`). Listeners catch it synchronously, evaluate their state, and push results/references directly to the mutable array in `e.detail`.
633
+ - **Bubbling:** `true` (to allow components/listeners attached to form or ancestor nodes to catch it).
634
+
635
+ ### Form Scoping and Validation Rules
636
+ - **Novalidate:** `ln-validate` injects `novalidate` on its host `<form>` automatically — the moment a form contains at least one `data-ln-validate` field, native browser bubbles are silenced in favor of inline error lists. Authors never write `novalidate` by hand; a form with zero `data-ln-validate` fields keeps native browser validation as the default.
637
+
628
638
  ---
629
639
 
630
640
  ## Coordinator — Full Example
@@ -653,20 +663,11 @@ nav.dispatchEvent(new CustomEvent('ln-profile:request-create', {
653
663
  }
654
664
  });
655
665
 
656
- // 2. Form submit → request event
657
- document.addEventListener('ln-form:submit', function (e) {
658
- if (e.target.getAttribute('data-ln-form') !== 'new-profile') return;
659
- const input = document.querySelector('[data-ln-field="new-profile-name"]');
660
- const name = input ? input.value.trim() : '';
661
- if (!name) return;
662
-
663
- const nav = _getNav();
664
- if (nav) {
665
- nav.dispatchEvent(new CustomEvent('ln-profile:request-create', {
666
- detail: { name: name }
667
- }));
668
- }
669
- input.value = '';
666
+ // 2. Local-first write form → ln-data-coordinator claims the native
667
+ // submit (preventDefault); react to the store outcome instead.
668
+ // <form data-ln-form data-ln-form-scope="profiles" method="post" action="/api/profiles">
669
+ document.addEventListener('ln-data-store:created', function (e) {
670
+ if (e.detail.store !== 'profiles') return;
670
671
  document.getElementById('modal-new-profile').setAttribute('data-ln-modal', 'close');
671
672
  });
672
673
 
@@ -828,14 +829,14 @@ Beyond text, `fill()` binds attributes (`data-ln-attr`) and toggles classes (`da
828
829
  <li class="ln-upload__item"
829
830
  data-ln-class="ln-upload__item--uploading:uploading, ln-upload__item--error:error, ln-upload__item--deleting:deleting">
830
831
  <svg class="ln-icon" aria-hidden="true">
831
- <use data-ln-attr="href:iconHref" href="#ln-file"></use>
832
+ <use data-ln-attr="href:iconHref" href="#ln-icon-file"></use>
832
833
  </svg>
833
834
  <span class="ln-upload__name" data-ln-field="name"></span>
834
835
  <span class="ln-upload__size" data-ln-field="sizeText"></span>
835
836
  <button type="button" class="ln-upload__remove"
836
837
  data-ln-upload-action="remove"
837
838
  data-ln-attr="aria-label:removeLabel, title:removeLabel">
838
- <svg class="ln-icon" aria-hidden="true"><use href="#ln-x"></use></svg>
839
+ <svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>
839
840
  </button>
840
841
  <div class="ln-upload__progress">
841
842
  <div class="ln-upload__progress-bar"></div>
@@ -851,7 +852,7 @@ li.setAttribute('data-file-id', localId);
851
852
  fill(li, {
852
853
  name: file.name,
853
854
  sizeText: '0%',
854
- iconHref: '#' + iconId, // → <use href="#lnc-file-pdf">
855
+ iconHref: '#' + iconId, // → <use href="#ln-icon-custom-file-pdf">
855
856
  removeLabel: dict.remove, // → aria-label + title on button
856
857
  uploading: true, // → adds ln-upload__item--uploading
857
858
  error: false,
@@ -875,14 +876,14 @@ One `fill()` call handles all three binding types at once:
875
876
  - **`data-ln-attr="href:iconHref"`** → one or more attributes per element (`href` on `<use>`, `aria-label` + `title` on the button)
876
877
  - **`data-ln-class="ln-upload__item--uploading:uploading, ..."`** → conditional class toggles driven by booleans
877
878
 
878
- State transitions are just successive `fill()` calls with different values — no imperative `classList.add/remove`, no manual `setAttribute`. The icon swap via `data-ln-attr="href:iconHref"` on a `<use>` element works because `ln-icons` runs a MutationObserver on `<use href>` changes and auto-fetches the new sprite — the component never touches the icon loader directly.
879
+ State transitions are just successive `fill()` calls with different values — no imperative `classList.add/remove`, no manual `setAttribute`. The icon swap via `data-ln-attr="href:iconHref"` on a `<use>` element works because `ln-icon` runs a MutationObserver on `<use href>` changes and auto-fetches the new sprite — the component never touches the icon loader directly.
879
880
 
880
881
  **Behavioral hooks live on attributes, not classes** — `data-ln-upload-action="remove"` is a JS query hook, not a fill slot. Delegated click handlers on `.ln-upload__list` use `e.target.closest('[data-ln-upload-action="remove"]')` to locate the button, which also matches when the click lands on a nested `<svg>` / `<use>`. Keeping behavioral hooks off CSS class names means a project can rename or restyle classes without breaking JS.
881
882
 
882
883
  **Imperative escape hatches** — two things `fill()` can't express cleanly:
883
884
 
884
885
  1. `removeBtn.disabled = !uploaded` — the `disabled` boolean attribute doesn't round-trip through `data-ln-attr` (setting vs. removing).
885
- 2. `progressBar.style.width = percent + '%'` — a continuous animation value, not discrete state. Same precedent as `ln-data-table` virtual-scroll spacer heights.
886
+ 2. `progressBar.style.width = percent + '%'` — a continuous animation value, not discrete state. Same precedent as `ln-table` virtual-scroll spacer heights.
886
887
 
887
888
  Both are one-line assignments right after the `fill()` call and are the only non-declarative touches in the render path.
888
889
 
@@ -900,7 +901,7 @@ Both are one-line assignments right after the `fill()` call and are the only non
900
901
  <li class="ln-upload__item"
901
902
  data-ln-class="ln-upload__item--uploading:uploading, ln-upload__item--error:error, ln-upload__item--deleting:deleting">
902
903
  <svg class="ln-icon ln-icon--lg" aria-hidden="true">
903
- <use data-ln-attr="href:iconHref" href="#ln-file"></use>
904
+ <use data-ln-attr="href:iconHref" href="#ln-icon-file"></use>
904
905
  </svg>
905
906
  <article>
906
907
  <p class="ln-upload__name" data-ln-field="name"></p>
@@ -909,7 +910,7 @@ Both are one-line assignments right after the `fill()` call and are the only non
909
910
  <button type="button" class="ln-upload__remove"
910
911
  data-ln-upload-action="remove"
911
912
  data-ln-attr="aria-label:removeLabel, title:removeLabel">
912
- <svg class="ln-icon" aria-hidden="true"><use href="#ln-x"></use></svg>
913
+ <svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>
913
914
  </button>
914
915
  <div class="ln-upload__progress"><div class="ln-upload__progress-bar"></div></div>
915
916
  </li>
@@ -956,7 +957,7 @@ The component calls `_ensureDefaultItemTemplate()` at the top of `_initUpload()`
956
957
  | Component | Template(s) | Lookup | Notes |
957
958
  |---|---|---|---|
958
959
  | **ln-upload** | `ln-upload-item` | `cloneTemplateScoped` | Scoped → global → auto-injected default on first init |
959
- | **ln-data-table** | `{name}-row`, `{name}-empty`, `{name}-empty-filtered`, `column-filter` (also falls back to `{name}-column-filter`) | `cloneTemplateScoped` | Scoped-first per table instance |
960
+ | **ln-table** | `{name}-row`, `{name}-empty`, `{name}-empty-filtered`, `column-filter` (also falls back to `{name}-column-filter`) | `cloneTemplateScoped` | Scoped-first per table instance |
960
961
  | **ln-translations** | `ln-translations-menu-item`, `ln-translations-badge` | `cloneTemplate` | Single global template, no per-instance override |
961
962
 
962
963
  ### Rules
@@ -983,10 +984,10 @@ The component calls `_ensureDefaultItemTemplate()` at the top of `_initUpload()`
983
984
 
984
985
  | Component | Pattern | Data Attr | Description |
985
986
  |-----------|---------|-----------|------|
986
- | ln-core | Shared module | — | cloneTemplate, cloneTemplateScoped, dispatch, dispatchCancelable, fill, fillTemplate, renderList, buildDict, guardBody, findElements, reactiveState, deepReactive, createBatcher |
987
+ | ln-core | Shared module | — | cloneTemplate, cloneTemplateScoped, dispatch, dispatchCancelable, fill, fillTemplate, renderList, buildDict, guardBody, findElements, reactiveState, deepReactive, createBatcher, createWindowCache |
987
988
  | ln-toggle | Instance | `data-ln-toggle` | Generic toggle (sidebar, collapse) |
988
989
  | ln-accordion | Instance | `data-ln-accordion` | Wrapper — only one toggle open at a time |
989
- | ln-tabs | Instance | `data-ln-tabs` | Hash-aware tab navigation |
990
+ | ln-tabs | Instance | `data-ln-tabs` | Tab navigation — URL hash (anchor triggers) or localStorage persist (button triggers) |
990
991
  | ln-nav | Instance | `data-ln-nav` | Active link highlighter |
991
992
  | ln-modal | Instance | `data-ln-modal` | Modal dialog |
992
993
  | ln-toast | Functional | `data-ln-toast` | Toast notifications |
@@ -996,8 +997,8 @@ The component calls `_ensureDefaultItemTemplate()` at the top of `_initUpload()`
996
997
  | ln-circular-progress | Instance | `data-ln-circular-progress` | Circular (ring) progress indicator |
997
998
  | ln-search | Instance | `data-ln-search` | Generic search (textContent filter) |
998
999
  | ln-filter | Instance | `data-ln-filter` | Generic filter (data attribute filter) |
999
- | ln-table | Instance | `data-ln-table` | Data table (search, filter, sort, virtual scroll) |
1000
- | ln-table-sort | Instance | `data-ln-sort` | Sort header handler (companion to ln-table) |
1000
+ | ln-table | Instance | `data-ln-table` | Data table (search, filter, sort, virtual scroll; opt-in server-side windowed virtualization via `data-ln-table-window`) |
1001
+ | ln-sort | Instance | `data-ln-sort` | Sort control primitive — 3-trigger cycle (none→asc→desc→none), search-precedent cancelable event; targets ln-table, ln-list, or plain lists |
1001
1002
  | ln-sortable | Instance | `data-ln-sortable` | Drag & drop reorder |
1002
1003
  | ln-dropdown | Instance | `data-ln-dropdown` | Positioned dropdown menu (wraps ln-toggle) |
1003
1004
  | ln-popover | Instance | `data-ln-popover` | Rich popover with viewport-aware positioning and ESC-stack management |
@@ -1049,42 +1050,47 @@ own attribute (`data-ln-modal`, `data-ln-popover`, `data-ln-tabs-active`).
1049
1050
  The "attribute is the single source of truth" pattern is shared; the
1050
1051
  specific attribute is not.
1051
1052
 
1052
- ### Form family — ln-form coordinates siblings
1053
+ ### Form family — ln-validate owns the submit gate
1053
1054
 
1054
1055
  ```
1055
- ln-form (catches submit, reads ln-validate events)
1056
- ├── ln-validate dispatches ln-validate:valid / ln-validate:invalid,
1057
- │ which ln-form listens for
1058
- └── ln-autosave stores form state in localStorage; writes on blur/change
1059
- independently of ln-form
1056
+ ln-validate (first field per form attaches the submit gate;
1057
+ every field self-registers its own request-validate
1058
+ listener)
1059
+
1060
+ ln-form (populate + RESTful action routing only — no submit
1061
+ listener, no validation awareness)
1062
+
1063
+ ln-autosave stores form state in localStorage; writes on blur/change,
1064
+ independently of both ln-form and ln-validate
1060
1065
  ```
1061
1066
 
1062
- ln-form never calls `lnValidate.isValid()` or reads TomSelect state
1063
- directly. It listens to the validation events and lets each sibling
1064
- manage its own internals.
1067
+ None of the three import each other or call cross-instance methods.
1068
+ `ln-form.fill()` dispatches synthetic `input`/`change` on populated
1069
+ fields; `ln-validate`'s own field listeners react to those exactly as
1070
+ they would to real user input. `ln-autosave` does the same on restore.
1065
1071
 
1066
- ### Data family — ln-data-table ↔ ln-store through the coordinator
1072
+ ### Data family — ln-table ↔ ln-store through the coordinator
1067
1073
 
1068
1074
  ```
1069
- ln-data-table ln-store
1075
+ ln-table ln-store
1070
1076
  │ │
1071
- │ ln-data-table:request-data ─────────▶│
1077
+ │ ln-table:request-data ──────────────▶│
1072
1078
  │ (sort, filters, search) │ (reads IndexedDB)
1073
1079
  │ │
1074
- │◀─────── ln-data-table:set-data │
1080
+ │◀─────── ln-table:set-data │
1075
1081
  │ (rows + totals) │
1076
1082
  │ │
1077
- │◀─────── ln-store:synced │
1083
+ │◀─────── ln-data-store:synced │
1078
1084
  │ (coordinator re-queries) │
1079
1085
  ▲
1080
1086
  │
1081
1087
  project coordinator
1082
1088
  ```
1083
1089
 
1084
- `ln-data-table` never imports or queries `ln-store`. It emits a request
1090
+ `ln-table` never imports or queries `ln-store`. It emits a request
1085
1091
  event; the project coordinator catches it, calls `storeEl.lnStore.getAll(...)`,
1086
- and dispatches `ln-data-table:set-data` back. See
1087
- [docs/js/component-guide.md](../docs/js/component-guide.md#data-flow-with-ln-store)
1092
+ and dispatches `ln-table:set-data` back. See
1093
+ [docs/architecture/component-guide.md](../docs/architecture/component-guide.md#data-flow-with-ln-store)
1088
1094
  for the full handshake.
1089
1095
 
1090
1096
  ### Rule
@@ -1099,4 +1105,4 @@ whole stack.
1099
1105
  ---
1100
1106
 
1101
1107
  > For the latest component skeleton and checklist →
1102
- > see [docs/js/component-guide.md](../docs/js/component-guide.md)
1108
+ > see [docs/architecture/component-guide.md](../docs/architecture/component-guide.md)
package/js/index.js CHANGED
@@ -3,8 +3,19 @@ import '../scss/ln-ashlar.scss';
3
3
 
4
4
  // JS Components
5
5
  import './ln-http/src/ln-http.js';
6
+ import './ln-form/src/ln-form.js';
7
+ // ln-validate must attach its submit listener before ln-ajax's: a <form>
8
+ // can carry both data-ln-validate fields and data-ln-ajax, and
9
+ // same-target same-event listeners fire in registration order.
10
+ // ln-validate's validation gate has to run first so ln-ajax can see
11
+ // e.defaultPrevented. ln-data-coordinator's own write-claim listener is
12
+ // unaffected by this ordering — it lives on `document` (bubble phase),
13
+ // strictly after every listener registered directly on the form itself.
14
+ import './ln-validate/src/ln-validate.js';
6
15
  import './ln-ajax/src/ln-ajax.js';
16
+ import './ln-router/src/ln-router.js';
7
17
  import './ln-modal/src/ln-modal.js';
18
+ import './ln-modal-coordinator/src/ln-modal-coordinator.js';
8
19
  import './ln-number/src/ln-number.js';
9
20
  import './ln-date/src/ln-date.js';
10
21
  import './ln-nav/src/ln-nav.js';
@@ -21,21 +32,27 @@ import './ln-link/src/ln-link.js';
21
32
  import './ln-progress/src/ln-progress.js';
22
33
  import './ln-filter/src/ln-filter.js';
23
34
  import './ln-search/src/ln-search.js';
24
- import './ln-table/src/ln-table-sort.js';
35
+ import './ln-sort/src/ln-sort.js';
25
36
  import './ln-table/src/ln-table.js';
37
+ import './ln-table-coordinator/src/ln-table-coordinator.js';
38
+ import './ln-list/src/ln-list.js';
26
39
  import './ln-circular-progress/src/ln-circular-progress.js';
27
40
  import './ln-sortable/src/ln-sortable.js';
28
41
  import './ln-confirm/src/ln-confirm.js';
29
42
  import './ln-translations/src/ln-translations.js';
30
43
  import './ln-autosave/src/ln-autosave.js';
31
44
  import './ln-autoresize/src/ln-autoresize.js';
32
- import './ln-validate/src/ln-validate.js';
33
- import './ln-form/src/ln-form.js';
45
+ import './ln-editor/src/ln-editor.js';
46
+ import './ln-fill/src/ln-fill.js';
47
+ import './ln-slug/src/ln-slug.js';
34
48
  import './ln-time/src/ln-time.js';
35
49
  import './ln-data-store/src/ln-data-store.js';
36
50
  import './ln-api-connector/src/ln-api-connector.js';
37
51
  import './ln-couchdb-connector/src/ln-couchdb-connector.js';
38
52
  import './ln-data-coordinator/src/ln-data-coordinator.js';
39
- import './ln-data-table/src/ln-data-table.js';
40
- import './ln-icons/src/ln-icons.js';
53
+ import './ln-api-queue/src/ln-api-queue.js';
54
+ import './ln-options/src/ln-options.js';
55
+ import './ln-stat/src/ln-stat.js';
56
+ import './ln-icon/src/ln-icon.js';
57
+ import './ln-debug/src/ln-debug.js';
41
58
 
@@ -1,137 +1,186 @@
1
- # ln-accordion
2
-
3
- > A lightweight, stateless **Coordinator** that enforces a single-open rule across a list of independent `ln-toggle` panels.
4
-
5
- ---
6
-
7
- ## 1. Philosophy & The Coordinator Mindset
8
-
9
- In traditional frontend architectures, an accordion is a heavy, monolithic component that owns click events, height transitions, ARIA states, active-panel state, and storage persistence.
10
-
11
- In `ln-ashlar`, `ln-accordion` is fundamentally different: **it is a pure coordinator**. It contains only 38 lines of JavaScript, carries zero internal state, and coordinates other highly-specialized primitives.
12
-
13
- Every accordion is a perfect synchronization of three orthogonal concerns:
14
-
15
- 1. **State Primitive (`ln-toggle`)**: Each panel is an independent `ln-toggle` instance. It owns the binary `open`/`close` state, coordinates trigger buttons, synchronizes `aria-expanded`/`aria-controls`, and handles per-panel localStorage persistence (`data-ln-persist`). It is completely oblivious to the other panels or the fact that it is inside an accordion.
16
- 2. **Animation Engine (CSS `.collapsible`)**: The height transition is handled entirely in Vanilla CSS via the `.collapsible` mixin (transitioning `grid-template-rows` from `0fr` to `1fr`). No JS framerate stuttering or inline height hacks.
17
- 3. **The Coordinator (`ln-accordion`)**: Lives on the wrapper element. It enforces a single rule: *"When one panel opens, all other open panels in this group must close."*
18
-
19
- ---
20
-
21
- ## 2. Minimal Blueprint
22
-
23
- This is the standard HTML structure. The pairing of triggers and panels is by ID, keeping layout and proximity decoupled.
24
-
25
- ```html
26
- <ul data-ln-accordion>
27
- <li>
28
- <!-- The Trigger -->
29
- <header data-ln-toggle-for="panel1">
30
- Section 1
31
- <svg class="ln-icon ln-chevron" aria-hidden="true"><use href="#ln-arrow-down"></use></svg>
32
- </header>
33
- <!-- The Collapsible Panel -->
34
- <section id="panel1" data-ln-toggle="open" class="collapsible">
35
- <article class="collapsible-body">
36
- <p>Content 1 (Starts open).</p>
37
- </article>
38
- </section>
39
- </li>
40
- <li>
41
- <header data-ln-toggle-for="panel2">
42
- Section 2
43
- <svg class="ln-icon ln-chevron" aria-hidden="true"><use href="#ln-arrow-down"></use></svg>
44
- </header>
45
- <section id="panel2" data-ln-toggle class="collapsible">
46
- <article class="collapsible-body">
47
- <p>Content 2 (Starts closed).</p>
48
- </article>
49
- </section>
50
- </li>
51
- </ul>
52
- ```
53
-
54
- ### Key Anatomy Rules
55
- - **The Wrapper (`data-ln-accordion`)**: Markers for the coordinator. Listens for bubbled events and coordinates siblings.
56
- - **The Trigger (`data-ln-toggle-for`)**: Click target that toggles the target panel ID. The chevron rotates automatically driven by `aria-expanded`.
57
- - **The Panel (`data-ln-toggle`)**: Creates the `ln-toggle` state instance. Value `open` or empty.
58
- - **The Body (`.collapsible-body`)**: Wraps actual content. Padding and margins must live here, not on the parent `.collapsible` (which needs zero padding to collapse to exactly `0px` height).
59
-
60
- ---
61
-
62
- ## 3. The Decoupled State & API Contract
63
-
64
- The coordinator has **zero public state** in JavaScript. The DOM is the source of truth, and **the HTML attribute is the sole contract**.
65
-
66
- ### Attributes
67
- - `data-ln-accordion` on the wrapper creates the coordinator instance. It takes no values.
68
-
69
- ### Events
70
- - **`ln-accordion:change`**: Dispatched on the wrapper after a panel opens and siblings close.
71
- - `event.detail.target`: The HTML element of the panel that just opened.
72
- ```js
73
- document.addEventListener('ln-accordion:change', (e) => {
74
- console.log('Active panel ID:', e.detail.target.id);
75
- });
76
- ```
77
-
78
- ### Programmatic Control
79
- There are no `open()` or `close()` methods on the coordinator instance. To programmatically change panels, write directly to the target panel's attribute:
80
-
81
- ```js
82
- // The coordinator catches the bubbled event and closes all siblings automatically.
83
- document.getElementById('panel2').setAttribute('data-ln-toggle', 'open');
84
- ```
85
-
86
- ---
87
-
88
- ## 4. Integration Patterns
89
-
90
- ### A. All-Closed by Default
91
- Simply omit the `="open"` value from all panels in the markup.
92
- ```html
93
- <section id="panel1" data-ln-toggle class="collapsible">...</section>
94
- ```
95
-
96
- ### B. Persistent Accordion State (Across Page Reloads)
97
- Add `data-ln-persist` to the panels. Each panel saves its state in `localStorage` individually. The coordinator stays completely oblivious. On page load, whichever panel restores as `open` bubbles an event, and the coordinator handles the rest.
98
- ```html
99
- <section id="panel1" data-ln-toggle data-ln-persist class="collapsible">...</section>
100
- ```
101
-
102
- ### C. Zero-Configuration Multi-Open
103
- If your requirements change and you want a "multi-open accordion" (where panels toggle independently without closing others), **you do not need any JavaScript options or class re-configuration**. Simply *remove the `data-ln-accordion` attribute* from the wrapper. The individual panels continue to work perfectly.
104
-
105
- ### D. Nested Accordions
106
- Supported natively out of the box. Scoping is determined by DOM ancestry (using `element.closest('[data-ln-accordion]')`). Opening an inner accordion panel bubbles upwards, but the outer coordinator ignores it, allowing infinite nesting depth without any configuration.
107
-
108
- ---
109
-
110
- ## 5. Common Implementation Pitfalls
111
-
112
- ### 1. Padding on `.collapsible` directly
113
- The `.collapsible` container must have zero padding so it can transition to exactly `0px` height. Placing padding directly on `.collapsible` will cause a thin strip of content to remain visible even when closed. **Padding must live on the `.collapsible-body` child**.
114
-
115
- ### 2. Double-Binding Attributes
116
- Never place `data-ln-toggle` and `data-ln-toggle-for` on the same element. One element is either a trigger or a panel, never both.
117
-
118
- ---
119
-
120
- ## 6. Integration & Source Files
121
-
122
- - **Unified Bundle**: Loaded automatically with the main bundle:
123
- ```html
124
- <script src="dist/ln-ashlar.iife.js" defer></script>
125
- ```
126
- - **Standalone IIFE**: For lightweight pages, load the standalone, self-registering IIFE version:
127
- ```html
128
- <script src="js/ln-accordion/ln-accordion.js" defer></script>
129
- ```
130
- - **Active Source (ESM)**: Development source is located at [js/ln-accordion/src/ln-accordion.js](file:///c:/laragon/www/ln-ashlar/js/ln-accordion/src/ln-accordion.js).
131
-
132
- ---
133
-
134
- ## Related
135
- - **[`ln-toggle`](../ln-toggle/README.md)** — Binary state primitive.
136
- - **Architecture deep-dive** — [`docs/js/accordion.md`](../../docs/js/accordion.md).
137
- - **Cross-component principles** — [`docs/architecture/data-flow.md`](../../docs/architecture/data-flow.md).
1
+ # ln-accordion
2
+
3
+ > A lightweight, stateless **Coordinator** that enforces a single-open rule across a list of independent `ln-toggle` panels.
4
+
5
+ ---
6
+
7
+ ## 1. Philosophy & The Coordinator Mindset
8
+
9
+ In traditional frontend architectures, an accordion is a heavy, monolithic component that owns click events, height transitions, ARIA states, active-panel state, and storage persistence.
10
+
11
+ In `ln-ashlar`, `ln-accordion` is fundamentally different: **it is a pure coordinator**. It contains only 38 lines of JavaScript, carries zero internal state, and coordinates other highly-specialized primitives.
12
+
13
+ Every accordion is a perfect synchronization of three orthogonal concerns:
14
+
15
+ 1. **State Primitive (`ln-toggle`)**: Each panel is an independent `ln-toggle` instance. It owns the binary `open`/`close` state, coordinates trigger buttons, synchronizes `aria-expanded`/`aria-controls`, and handles per-panel localStorage persistence (`data-ln-persist`). It is completely oblivious to the other panels or the fact that it is inside an accordion.
16
+ 2. **Animation Engine (CSS `.collapsible`)**: The height transition is handled entirely in Vanilla CSS via the `.collapsible` mixin (transitioning `grid-template-rows` from `0fr` to `1fr`). No JS framerate stuttering or inline height hacks.
17
+ 3. **The Coordinator (`ln-accordion`)**: Lives on the wrapper element. It enforces a single rule: *"When one panel opens, all other open panels in this group must close."*
18
+
19
+ ---
20
+
21
+ ## 2. Minimal Blueprint
22
+
23
+ This is the standard HTML structure. The pairing of triggers and panels is by ID, keeping layout and proximity decoupled.
24
+
25
+ ```html
26
+ <ul data-ln-accordion>
27
+ <li>
28
+ <!-- The Trigger -->
29
+ <header data-ln-toggle-for="panel1">
30
+ Section 1
31
+ <svg class="ln-icon ln-chevron" aria-hidden="true"><use href="#ln-icon-arrow-down"></use></svg>
32
+ </header>
33
+ <!-- The Collapsible Panel -->
34
+ <section id="panel1" data-ln-toggle="open" class="collapsible">
35
+ <article class="collapsible-body">
36
+ <p>Content 1 (Starts open).</p>
37
+ </article>
38
+ </section>
39
+ </li>
40
+ <li>
41
+ <header data-ln-toggle-for="panel2">
42
+ Section 2
43
+ <svg class="ln-icon ln-chevron" aria-hidden="true"><use href="#ln-icon-arrow-down"></use></svg>
44
+ </header>
45
+ <section id="panel2" data-ln-toggle class="collapsible">
46
+ <article class="collapsible-body">
47
+ <p>Content 2 (Starts closed).</p>
48
+ </article>
49
+ </section>
50
+ </li>
51
+ </ul>
52
+ ```
53
+
54
+ ### Key Anatomy Rules
55
+ - **The Wrapper (`data-ln-accordion`)**: Markers for the coordinator. Listens for bubbled events and coordinates siblings.
56
+ - **The Trigger (`data-ln-toggle-for`)**: Click target that toggles the target panel ID. The chevron rotates automatically driven by `aria-expanded`.
57
+ - **The Panel (`data-ln-toggle`)**: Creates the `ln-toggle` state instance. Value `open` or empty.
58
+ - **The Body (`.collapsible-body`)**: Wraps actual content. Padding and margins must live here, not on the parent `.collapsible` (which needs zero padding to collapse to exactly `0px` height).
59
+
60
+ ---
61
+
62
+ ## 3. The Decoupled State & API Contract
63
+
64
+ The coordinator has **zero public state** in JavaScript. The DOM is the source of truth, and **the HTML attribute is the sole contract**.
65
+
66
+ ### Attributes
67
+ - `data-ln-accordion` on the wrapper creates the coordinator instance. It takes no values.
68
+
69
+ ### Events
70
+ - **`ln-accordion:change`**: Dispatched on the wrapper after a panel opens and siblings close.
71
+ - `event.detail.target`: The HTML element of the panel that just opened.
72
+ ```js
73
+ document.addEventListener('ln-accordion:change', (e) => {
74
+ console.log('Active panel ID:', e.detail.target.id);
75
+ });
76
+ ```
77
+
78
+ ### Programmatic Control
79
+ There are no `open()` or `close()` methods on the coordinator instance. To programmatically change panels, write directly to the target panel's attribute:
80
+
81
+ ```js
82
+ // The coordinator catches the bubbled event and closes all siblings automatically.
83
+ document.getElementById('panel2').setAttribute('data-ln-toggle', 'open');
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 4. Integration Patterns
89
+
90
+ ### A. All-Closed by Default
91
+ Simply omit the `="open"` value from all panels in the markup.
92
+ ```html
93
+ <section id="panel1" data-ln-toggle class="collapsible">...</section>
94
+ ```
95
+
96
+ ### B. Persistent Accordion State (Across Page Reloads)
97
+ Add `data-ln-persist` to the panels. Each panel saves its state in `localStorage` individually. The coordinator stays completely oblivious. On page load, whichever panel restores as `open` bubbles an event, and the coordinator handles the rest.
98
+ ```html
99
+ <section id="panel1" data-ln-toggle data-ln-persist class="collapsible">...</section>
100
+ ```
101
+
102
+ ### C. Zero-Configuration Multi-Open
103
+ If your requirements change and you want a "multi-open accordion" (where panels toggle independently without closing others), **you do not need any JavaScript options or class re-configuration**. Simply *remove the `data-ln-accordion` attribute* from the wrapper. The individual panels continue to work perfectly.
104
+
105
+ ### D. Nested Accordions
106
+ Supported natively out of the box. Scoping is determined by DOM ancestry (using `element.closest('[data-ln-accordion]')`). Opening an inner accordion panel bubbles upwards, but the outer coordinator ignores it, allowing infinite nesting depth without any configuration.
107
+
108
+ ---
109
+
110
+ ## 5. Common Implementation Pitfalls
111
+
112
+ ### 1. Padding on `.collapsible` directly
113
+ The `.collapsible` container must have zero padding so it can transition to exactly `0px` height. Placing padding directly on `.collapsible` will cause a thin strip of content to remain visible even when closed. **Padding must live on the `.collapsible-body` child**.
114
+
115
+ ### 2. Double-Binding Attributes
116
+ Never place `data-ln-toggle` and `data-ln-toggle-for` on the same element. One element is either a trigger or a panel, never both.
117
+
118
+ ---
119
+
120
+ ## 6. Integration & Source Files
121
+
122
+ - **Unified Bundle**: Loaded automatically with the main bundle:
123
+ ```html
124
+ <script src="dist/ln-ashlar.iife.js" defer></script>
125
+ ```
126
+ - **Standalone IIFE**: For lightweight pages, load the standalone, self-registering IIFE version:
127
+ ```html
128
+ <script src="js/ln-accordion/ln-accordion.js" defer></script>
129
+ ```
130
+ - **Active Source (ESM)**: Development source is located at [js/ln-accordion/src/ln-accordion.js](file:///c:/laragon/www/ln-ashlar/js/ln-accordion/src/ln-accordion.js).
131
+
132
+ ---
133
+
134
+ ## Related
135
+ - **[`ln-toggle`](../ln-toggle/README.md)** — Binary state primitive.
136
+ - **Cross-component principles** — [`docs/architecture/data-flow.md`](../../docs/architecture/data-flow.md).
137
+
138
+ ---
139
+
140
+ ## 🔧 Internals
141
+
142
+ Source: `js/ln-accordion/ln-accordion.js`. Does not import or require any other component at runtime — it only needs `ln-toggle` loaded so the events it listens for exist (a consumer-side load-order coupling, not a source dependency).
143
+
144
+ ### Instance state
145
+
146
+ | Field | Set by | Read by |
147
+ |---|---|---|
148
+ | `dom` | constructor argument | `destroy` (dispatch target, listener detach) |
149
+ | `_onToggleOpen` | constructor | `addEventListener`/`removeEventListener` |
150
+
151
+ That's the entirety of it — no cached toggle list, no active-panel pointer, no DOM reference cache. `dom.querySelectorAll('[data-ln-toggle]')` is re-run on every `ln-toggle:open` instead of cached, since the DOM is small and the event is user-driven (one per click).
152
+
153
+ ### Init
154
+
155
+ `registerComponent` scans for `[data-ln-accordion]`, instantiates `_component(el)` per match, and reruns the scan via `MutationObserver` on `childList` add / `data-ln-accordion` attribute mutation. `_component(dom)` stores `dom`, binds `_onToggleOpen`, and listens for `ln-toggle:open` on `dom`. No initial DOM read, no event dispatched at init.
156
+
157
+ ### Reaction to a panel opening
158
+
159
+ `ln-toggle` dispatches `ln-toggle:open` on the panel, which bubbles to the wrapper:
160
+
161
+ 1. `dom.querySelectorAll('[data-ln-toggle]')` — every toggle in the subtree.
162
+ 2. For each toggle that is not the one just opened and is currently `"open"`: `setAttribute('data-ln-toggle', 'close')`.
163
+ 3. Each write triggers that toggle's own `MutationObserver`-driven close pipeline (`before-close` cancelable event, class removal, `aria-expanded` sync, persistence write, `ln-toggle:close`) — the accordion never runs this logic itself, it only flips the attribute.
164
+ 4. After the loop: dispatch `ln-accordion:change` on the wrapper, `detail.target` = the panel that opened.
165
+
166
+ The accordion is reactive, not a gate: cancelable toggle events are the toggle's own responsibility, and `:change` fires regardless of whether a `before-close` on a sibling gets cancelled.
167
+
168
+ ### Destroy
169
+
170
+ Guards double-destroy, removes the `ln-toggle:open` listener, dispatches `ln-accordion:destroyed` (`detail.target` = the wrapper), deletes the instance reference. Child `ln-toggle` instances are untouched and keep working independently.
171
+
172
+ ### Event lifecycle
173
+
174
+ | Event | Direction | `detail` | Notes |
175
+ |---|---|---|---|
176
+ | `ln-toggle:open` | in (bubble) | — | Only inbound event; `ln-toggle:close` is not listened to |
177
+ | `ln-accordion:change` | out | `{ target }` = opened panel | Fires on every open, even if no sibling needed closing |
178
+ | `ln-accordion:destroyed` | out | `{ target }` = wrapper | Inside `destroy()` |
179
+
180
+ ### Nested accordions
181
+
182
+ Two `closest('[data-ln-accordion]')` ownership checks in `_onToggleOpen`: an early exit if the bubbled event's nearest accordion isn't `dom` (so an outer wrapper ignores an inner panel's open), and a per-candidate filter in the cascade scan (so nested-accordion toggles are skipped when closing siblings). Ownership is DOM ancestry, not JS bookkeeping — each accordion stays oblivious to any other accordion in the tree, supporting arbitrary nesting depth.
183
+
184
+ ### Why a separate `ln-toggle`
185
+
186
+ Most toggle use cases (sidebars, dropdowns, expandable cards) aren't accordions. Splitting single-open coordination out keeps `ln-toggle` a pure individual-state primitive and lets `ln-accordion` opt in to the extra behavior at the wrapper level, driven entirely by attribute writes rather than instance method calls.