@livenetworks/ashlar 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +189 -177
- package/js/COMPONENTS.md +55 -49
- package/js/index.js +22 -5
- package/js/ln-accordion/README.md +186 -137
- package/js/ln-accordion/ln-accordion.js +1 -1
- package/js/ln-ajax/README.md +73 -4
- package/js/ln-ajax/ln-ajax.js +1 -1
- package/js/ln-ajax/src/ln-ajax.js +13 -13
- package/js/ln-api-connector/README.md +261 -150
- package/js/ln-api-connector/ln-api-connector.js +1 -1
- package/js/ln-api-connector/src/ln-api-connector.js +368 -265
- package/js/ln-api-queue/README.md +219 -0
- package/js/ln-api-queue/ln-api-queue.js +1 -0
- package/js/ln-api-queue/src/ln-api-queue.js +320 -0
- package/js/ln-api-queue/src/queue-storage.js +455 -0
- package/js/ln-autoresize/README.md +47 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -1
- package/js/ln-autosave/README.md +33 -1
- package/js/ln-autosave/ln-autosave.js +1 -1
- package/js/ln-autosave/src/ln-autosave.js +4 -4
- package/js/ln-circular-progress/README.md +27 -13
- package/js/ln-circular-progress/ln-circular-progress.js +1 -1
- package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
- package/js/ln-confirm/README.md +162 -86
- package/js/ln-confirm/_ln-confirm.scss +11 -0
- package/js/ln-confirm/ln-confirm.js +1 -1
- package/js/ln-confirm/src/ln-confirm.js +77 -41
- package/js/ln-core/README.md +896 -0
- package/js/ln-core/hash.js +92 -0
- package/js/ln-core/helpers.js +337 -8
- package/js/ln-core/index.js +4 -2
- package/js/ln-core/persist.js +29 -3
- package/js/ln-core/positioning.js +1 -35
- package/js/ln-core/window-cache.js +213 -0
- package/js/ln-couchdb-connector/README.md +177 -156
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
- package/js/ln-data-coordinator/README.md +536 -165
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
- package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
- package/js/ln-data-store/README.md +241 -94
- package/js/ln-data-store/ln-data-store.js +1 -1
- package/js/ln-data-store/src/ln-data-store.js +694 -699
- package/js/ln-date/README.md +39 -1
- package/js/ln-date/ln-date-dev.scss +14 -0
- package/js/ln-date/ln-date.js +1 -1
- package/js/ln-date/src/ln-date.js +236 -178
- package/js/ln-debug/README.md +58 -0
- package/js/ln-debug/ln-debug.js +1 -0
- package/js/ln-debug/src/ln-debug.js +19 -0
- package/js/ln-dropdown/README.md +31 -8
- package/js/ln-dropdown/ln-dropdown.js +1 -1
- package/js/ln-dropdown/src/ln-dropdown.js +30 -13
- package/js/ln-editor/README.md +274 -0
- package/js/ln-editor/ln-editor.js +1 -0
- package/js/ln-editor/ln-editor.scss +43 -0
- package/js/ln-editor/src/ln-editor.js +619 -0
- package/js/ln-external-links/README.md +44 -5
- package/js/ln-external-links/ln-external-links.js +1 -1
- package/js/ln-fill/README.md +199 -0
- package/js/ln-fill/ln-fill.js +1 -0
- package/js/ln-fill/src/ln-fill.js +87 -0
- package/js/ln-filter/README.md +92 -2
- package/js/ln-filter/ln-filter-dev.scss +8 -0
- package/js/ln-filter/ln-filter.js +1 -1
- package/js/ln-filter/src/ln-filter.js +22 -7
- package/js/ln-form/README.md +212 -49
- package/js/ln-form/ln-form.js +1 -1
- package/js/ln-form/src/ln-form.js +99 -199
- package/js/ln-http/README.md +125 -89
- package/js/ln-http/ln-http.js +1 -1
- package/js/ln-http/src/ln-http.js +32 -13
- package/js/ln-icon/README.md +130 -0
- package/js/ln-icon/ln-icon.js +1 -0
- package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
- package/js/ln-link/README.md +47 -9
- package/js/ln-link/ln-link.js +1 -1
- package/js/ln-link/src/ln-link.js +22 -6
- package/js/ln-list/README.md +129 -0
- package/js/ln-list/ln-list.js +1 -0
- package/js/ln-list/ln-list.scss +28 -0
- package/js/ln-list/src/ln-list.js +1216 -0
- package/js/ln-modal/README.md +185 -154
- package/js/ln-modal/ln-modal-dev.scss +13 -0
- package/js/ln-modal/ln-modal.js +1 -1
- package/js/ln-modal/ln-modal.scss +28 -4
- package/js/ln-modal/src/ln-modal.js +137 -201
- package/js/ln-modal-coordinator/README.md +99 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
- package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
- package/js/ln-nav/README.md +46 -3
- package/js/ln-nav/ln-nav.js +1 -1
- package/js/ln-nav/src/ln-nav.js +92 -127
- package/js/ln-number/README.md +28 -1
- package/js/ln-number/ln-number-dev.scss +13 -0
- package/js/ln-number/ln-number.js +1 -1
- package/js/ln-number/src/ln-number.js +443 -350
- package/js/ln-options/README.md +66 -0
- package/js/ln-options/ln-options.js +1 -0
- package/js/ln-options/src/ln-options.js +74 -0
- package/js/ln-popover/README.md +40 -5
- package/js/ln-popover/ln-popover.js +1 -1
- package/js/ln-popover/src/ln-popover.js +38 -11
- package/js/ln-progress/README.md +413 -442
- package/js/ln-progress/ln-progress.js +1 -1
- package/js/ln-progress/src/ln-progress.js +98 -150
- package/js/ln-router/README.md +432 -0
- package/js/ln-router/ln-router.js +1 -0
- package/js/ln-router/src/ln-router.js +539 -0
- package/js/ln-search/README.md +158 -83
- package/js/ln-search/ln-search-dev.scss +8 -0
- package/js/ln-search/ln-search.js +1 -1
- package/js/ln-search/src/ln-search.js +12 -9
- package/js/ln-slug/README.md +110 -0
- package/js/ln-slug/ln-slug.js +1 -0
- package/js/ln-slug/src/ln-slug.js +74 -0
- package/js/ln-sort/README.md +170 -0
- package/js/ln-sort/ln-sort-dev.scss +8 -0
- package/js/ln-sort/ln-sort.js +1 -0
- package/js/ln-sort/src/ln-sort.js +178 -0
- package/js/ln-sortable/README.md +133 -95
- package/js/ln-sortable/ln-sortable.js +1 -1
- package/js/ln-stat/README.md +55 -0
- package/js/ln-stat/ln-stat.js +1 -0
- package/js/ln-stat/src/ln-stat.js +55 -0
- package/js/ln-table/README.md +285 -101
- package/js/ln-table/ln-table-dev.scss +17 -0
- package/js/ln-table/ln-table.js +1 -1
- package/js/ln-table/ln-table.scss +1 -1
- package/js/ln-table/src/ln-table.js +1493 -473
- package/js/ln-table-coordinator/README.md +73 -0
- package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
- package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
- package/js/ln-tabs/README.md +221 -137
- package/js/ln-tabs/ln-tabs-dev.scss +18 -0
- package/js/ln-tabs/ln-tabs.js +1 -1
- package/js/ln-tabs/src/ln-tabs.js +41 -24
- package/js/ln-time/README.md +14 -0
- package/js/ln-time/ln-time.js +1 -1
- package/js/ln-time/src/ln-time.js +223 -192
- package/js/ln-toast/README.md +178 -122
- package/js/ln-toast/ln-toast.js +1 -15
- package/js/ln-toast/src/ln-toast.js +232 -210
- package/js/ln-toast/template.html +16 -12
- package/js/ln-toggle/README.md +61 -2
- package/js/ln-toggle/ln-toggle-dev.scss +13 -0
- package/js/ln-toggle/ln-toggle.js +1 -1
- package/js/ln-toggle/src/ln-toggle.js +56 -43
- package/js/ln-tooltip/README.md +95 -58
- package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -1
- package/js/ln-tooltip/src/ln-tooltip.js +45 -6
- package/js/ln-translations/README.md +118 -96
- package/js/ln-translations/ln-translations.js +1 -1
- package/js/ln-translations/src/ln-translations.js +10 -5
- package/js/ln-upload/README.md +85 -10
- package/js/ln-upload/ln-upload.js +1 -1
- package/js/ln-upload/src/ln-upload.js +26 -10
- package/js/ln-validate/README.md +72 -0
- package/js/ln-validate/ln-validate-dev.scss +15 -0
- package/js/ln-validate/ln-validate.js +1 -1
- package/js/ln-validate/src/ln-validate.js +64 -0
- package/package.json +61 -56
- package/scss/base/_global.scss +2 -2
- package/scss/base/_typography.scss +1 -2
- package/scss/components/_date.scss +1 -0
- package/scss/components/_dropdown.scss +0 -5
- package/scss/components/_editor.scss +5 -0
- package/scss/components/_filter.scss +26 -25
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +13 -3
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +36 -23
- package/scss/components/_toast.scss +68 -41
- package/scss/components/_toggle.scss +16 -0
- package/scss/config/_density.scss +205 -117
- package/scss/config/_icons.scss +10 -9
- package/scss/config/_tokens.scss +69 -47
- package/scss/config/mixins/_accordion.scss +2 -7
- package/scss/config/mixins/_alert.scss +4 -4
- package/scss/config/mixins/_breadcrumbs.scss +2 -2
- package/scss/config/mixins/_btn.scss +1 -1
- package/scss/config/mixins/_card.scss +89 -14
- package/scss/config/mixins/_collapsible.scss +3 -0
- package/scss/config/mixins/_colors.scss +1 -1
- package/scss/config/mixins/_diagnostics.scss +11 -0
- package/scss/config/mixins/_display.scss +1 -0
- package/scss/config/mixins/_dropdown.scss +3 -5
- package/scss/config/mixins/_editor.scss +124 -0
- package/scss/config/mixins/_empty-state.scss +7 -0
- package/scss/config/mixins/_filter.scss +3 -5
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +2 -0
- package/scss/config/mixins/_ln-table.scss +41 -2
- package/scss/config/mixins/_modal.scss +149 -66
- package/scss/config/mixins/_nav.scss +3 -4
- package/scss/config/mixins/_popover.scss +39 -0
- package/scss/config/mixins/_sidebar.scss +1 -1
- package/scss/config/mixins/_sort.scss +42 -0
- package/scss/config/mixins/_table.scss +85 -60
- package/scss/config/mixins/_toast.scss +179 -152
- package/scss/config/mixins/_tooltip.scss +17 -7
- package/scss/config/mixins/_typography.scss +4 -2
- package/scss/ln-ashlar-dev.scss +27 -9
- package/scss/ln-ashlar.scss +7 -1
- package/js/ln-data-table/README.md +0 -110
- package/js/ln-data-table/ln-data-table.js +0 -1
- package/js/ln-data-table/ln-data-table.scss +0 -10
- package/js/ln-data-table/src/ln-data-table.js +0 -1087
- package/js/ln-icons/README.md +0 -88
- package/js/ln-icons/ln-icons.js +0 -1
- package/js/ln-table/ln-table-sort.js +0 -1
- 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. **
|
|
173
|
-
3. **
|
|
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-
|
|
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 | `
|
|
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.
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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="#
|
|
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-
|
|
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-
|
|
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-
|
|
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` |
|
|
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-
|
|
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-
|
|
1053
|
+
### Form family — ln-validate owns the submit gate
|
|
1053
1054
|
|
|
1054
1055
|
```
|
|
1055
|
-
ln-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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-
|
|
1072
|
+
### Data family — ln-table ↔ ln-store through the coordinator
|
|
1067
1073
|
|
|
1068
1074
|
```
|
|
1069
|
-
ln-
|
|
1075
|
+
ln-table ln-store
|
|
1070
1076
|
│ │
|
|
1071
|
-
│ ln-
|
|
1077
|
+
│ ln-table:request-data ──────────────▶│
|
|
1072
1078
|
│ (sort, filters, search) │ (reads IndexedDB)
|
|
1073
1079
|
│ │
|
|
1074
|
-
│◀─────── ln-
|
|
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-
|
|
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-
|
|
1087
|
-
[docs/
|
|
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/
|
|
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-
|
|
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-
|
|
33
|
-
import './ln-
|
|
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-
|
|
40
|
-
import './ln-
|
|
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
|
-
- **
|
|
137
|
-
|
|
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.
|