@livenetworks/ashlar 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -0
- package/js/COMPONENTS.md +1102 -0
- package/js/index.js +41 -0
- package/js/ln-accordion/README.md +137 -0
- package/js/ln-accordion/ln-accordion.js +1 -0
- package/js/ln-accordion/src/ln-accordion.js +41 -0
- package/js/ln-ajax/README.md +91 -0
- package/js/ln-ajax/ln-ajax.js +1 -0
- package/js/ln-ajax/src/ln-ajax.js +277 -0
- package/js/ln-api-connector/README.md +150 -0
- package/js/ln-api-connector/ln-api-connector.js +1 -0
- package/js/ln-api-connector/src/ln-api-connector.js +265 -0
- package/js/ln-autoresize/README.md +80 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -0
- package/js/ln-autoresize/src/ln-autoresize.js +47 -0
- package/js/ln-autosave/README.md +92 -0
- package/js/ln-autosave/ln-autosave.js +1 -0
- package/js/ln-autosave/src/ln-autosave.js +147 -0
- package/js/ln-circular-progress/README.md +161 -0
- package/js/ln-circular-progress/ln-circular-progress.js +1 -0
- package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
- package/js/ln-confirm/README.md +86 -0
- package/js/ln-confirm/_ln-confirm.scss +13 -0
- package/js/ln-confirm/ln-confirm.js +1 -0
- package/js/ln-confirm/src/ln-confirm.js +131 -0
- package/js/ln-core/crypto.js +83 -0
- package/js/ln-core/helpers.js +411 -0
- package/js/ln-core/index.js +5 -0
- package/js/ln-core/persist.js +71 -0
- package/js/ln-core/positioning.js +207 -0
- package/js/ln-core/reactive.js +74 -0
- package/js/ln-couchdb-connector/README.md +156 -0
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
- package/js/ln-data-coordinator/README.md +165 -0
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
- package/js/ln-data-store/README.md +94 -0
- package/js/ln-data-store/ln-data-store.js +1 -0
- package/js/ln-data-store/src/ln-data-store.js +699 -0
- package/js/ln-data-table/README.md +110 -0
- package/js/ln-data-table/ln-data-table.js +1 -0
- package/js/ln-data-table/ln-data-table.scss +10 -0
- package/js/ln-data-table/src/ln-data-table.js +1103 -0
- package/js/ln-date/README.md +151 -0
- package/js/ln-date/ln-date.js +1 -0
- package/js/ln-date/src/ln-date.js +442 -0
- package/js/ln-dropdown/README.md +117 -0
- package/js/ln-dropdown/ln-dropdown.js +1 -0
- package/js/ln-dropdown/ln-dropdown.scss +15 -0
- package/js/ln-dropdown/src/ln-dropdown.js +174 -0
- package/js/ln-external-links/README.md +341 -0
- package/js/ln-external-links/ln-external-links.js +1 -0
- package/js/ln-external-links/src/ln-external-links.js +116 -0
- package/js/ln-filter/README.md +99 -0
- package/js/ln-filter/ln-filter.js +1 -0
- package/js/ln-filter/ln-filter.scss +7 -0
- package/js/ln-filter/src/ln-filter.js +404 -0
- package/js/ln-form/README.md +101 -0
- package/js/ln-form/ln-form.js +1 -0
- package/js/ln-form/src/ln-form.js +199 -0
- package/js/ln-http/README.md +89 -0
- package/js/ln-http/ln-http.js +1 -0
- package/js/ln-http/src/ln-http.js +219 -0
- package/js/ln-icons/README.md +88 -0
- package/js/ln-icons/ln-icons.js +1 -0
- package/js/ln-icons/src/ln-icons.js +169 -0
- package/js/ln-link/README.md +303 -0
- package/js/ln-link/ln-link.js +1 -0
- package/js/ln-link/src/ln-link.js +196 -0
- package/js/ln-modal/README.md +154 -0
- package/js/ln-modal/ln-modal.js +1 -0
- package/js/ln-modal/ln-modal.scss +11 -0
- package/js/ln-modal/src/ln-modal.js +201 -0
- package/js/ln-nav/README.md +70 -0
- package/js/ln-nav/ln-nav.js +1 -0
- package/js/ln-nav/src/ln-nav.js +177 -0
- package/js/ln-number/README.md +122 -0
- package/js/ln-number/ln-number.js +1 -0
- package/js/ln-number/src/ln-number.js +302 -0
- package/js/ln-popover/README.md +127 -0
- package/js/ln-popover/ln-popover.js +1 -0
- package/js/ln-popover/src/ln-popover.js +288 -0
- package/js/ln-progress/README.md +442 -0
- package/js/ln-progress/ln-progress.js +1 -0
- package/js/ln-progress/src/ln-progress.js +150 -0
- package/js/ln-search/README.md +83 -0
- package/js/ln-search/ln-search.js +1 -0
- package/js/ln-search/ln-search.scss +7 -0
- package/js/ln-search/src/ln-search.js +114 -0
- package/js/ln-sortable/README.md +95 -0
- package/js/ln-sortable/ln-sortable.js +1 -0
- package/js/ln-sortable/src/ln-sortable.js +203 -0
- package/js/ln-table/README.md +101 -0
- package/js/ln-table/ln-table-sort.js +1 -0
- package/js/ln-table/ln-table.js +1 -0
- package/js/ln-table/ln-table.scss +11 -0
- package/js/ln-table/src/ln-table-sort.js +168 -0
- package/js/ln-table/src/ln-table.js +473 -0
- package/js/ln-tabs/README.md +137 -0
- package/js/ln-tabs/ln-tabs.js +1 -0
- package/js/ln-tabs/src/ln-tabs.js +171 -0
- package/js/ln-time/README.md +81 -0
- package/js/ln-time/ln-time.js +1 -0
- package/js/ln-time/src/ln-time.js +192 -0
- package/js/ln-toast/README.md +122 -0
- package/js/ln-toast/ln-toast.js +15 -0
- package/js/ln-toast/src/ln-toast.js +210 -0
- package/js/ln-toast/template.html +14 -0
- package/js/ln-toggle/README.md +137 -0
- package/js/ln-toggle/ln-toggle.js +1 -0
- package/js/ln-toggle/src/ln-toggle.js +139 -0
- package/js/ln-tooltip/README.md +58 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -0
- package/js/ln-tooltip/ln-tooltip.scss +9 -0
- package/js/ln-tooltip/src/ln-tooltip.js +169 -0
- package/js/ln-translations/README.md +96 -0
- package/js/ln-translations/ln-translations.js +1 -0
- package/js/ln-translations/src/ln-translations.js +275 -0
- package/js/ln-upload/README.md +180 -0
- package/js/ln-upload/ln-upload.js +1 -0
- package/js/ln-upload/ln-upload.scss +20 -0
- package/js/ln-upload/src/ln-upload.js +407 -0
- package/js/ln-validate/README.md +108 -0
- package/js/ln-validate/ln-validate.js +1 -0
- package/js/ln-validate/src/ln-validate.js +160 -0
- package/package.json +55 -0
- package/scss/base/_global.scss +83 -0
- package/scss/base/_reset.scss +17 -0
- package/scss/base/_typography.scss +125 -0
- package/scss/components/_accordion.scss +34 -0
- package/scss/components/_ajax.scss +15 -0
- package/scss/components/_alert.scss +5 -0
- package/scss/components/_app-shell.scss +15 -0
- package/scss/components/_avatar.scss +6 -0
- package/scss/components/_breadcrumbs.scss +33 -0
- package/scss/components/_button.scss +20 -0
- package/scss/components/_card.scss +10 -0
- package/scss/components/_chip.scss +5 -0
- package/scss/components/_circular-progress.scss +29 -0
- package/scss/components/_confirm.scss +5 -0
- package/scss/components/_data-table.scss +83 -0
- package/scss/components/_dropdown.scss +25 -0
- package/scss/components/_empty-state.scss +22 -0
- package/scss/components/_form.scss +100 -0
- package/scss/components/_layout.scss +8 -0
- package/scss/components/_link.scss +11 -0
- package/scss/components/_ln-table.scss +60 -0
- package/scss/components/_loader.scss +6 -0
- package/scss/components/_modal.scss +20 -0
- package/scss/components/_nav.scss +9 -0
- package/scss/components/_page-header.scss +10 -0
- package/scss/components/_popover.scss +10 -0
- package/scss/components/_progress.scss +17 -0
- package/scss/components/_prose.scss +5 -0
- package/scss/components/_scrollbar.scss +32 -0
- package/scss/components/_sections.scss +12 -0
- package/scss/components/_sidebar.scss +5 -0
- package/scss/components/_stat-card.scss +5 -0
- package/scss/components/_status-badge.scss +4 -0
- package/scss/components/_stepper.scss +5 -0
- package/scss/components/_table.scss +19 -0
- package/scss/components/_tabs.scss +21 -0
- package/scss/components/_timeline.scss +14 -0
- package/scss/components/_toast.scss +41 -0
- package/scss/components/_toggle.scss +81 -0
- package/scss/components/_tooltip.scss +18 -0
- package/scss/components/_translations.scss +111 -0
- package/scss/components/_upload.scss +51 -0
- package/scss/config/_breakpoints.scss +72 -0
- package/scss/config/_density.scss +117 -0
- package/scss/config/_icons.scss +37 -0
- package/scss/config/_mixins.scss +13 -0
- package/scss/config/_theme.scss +216 -0
- package/scss/config/_tokens.scss +419 -0
- package/scss/config/mixins/_accordion.scss +52 -0
- package/scss/config/mixins/_ajax.scss +39 -0
- package/scss/config/mixins/_alert.scss +82 -0
- package/scss/config/mixins/_app-shell.scss +312 -0
- package/scss/config/mixins/_avatar.scss +109 -0
- package/scss/config/mixins/_borders.scss +36 -0
- package/scss/config/mixins/_breadcrumbs.scss +72 -0
- package/scss/config/mixins/_breakpoints.scss +62 -0
- package/scss/config/mixins/_btn.scss +179 -0
- package/scss/config/mixins/_card.scss +338 -0
- package/scss/config/mixins/_chip.scss +66 -0
- package/scss/config/mixins/_circular-progress.scss +71 -0
- package/scss/config/mixins/_collapsible.scss +24 -0
- package/scss/config/mixins/_colors.scss +46 -0
- package/scss/config/mixins/_confirm.scss +31 -0
- package/scss/config/mixins/_data-table.scss +346 -0
- package/scss/config/mixins/_display.scss +32 -0
- package/scss/config/mixins/_dropdown.scss +143 -0
- package/scss/config/mixins/_empty-state.scss +30 -0
- package/scss/config/mixins/_focus.scss +55 -0
- package/scss/config/mixins/_footer.scss +42 -0
- package/scss/config/mixins/_form.scss +601 -0
- package/scss/config/mixins/_index.scss +58 -0
- package/scss/config/mixins/_interaction.scss +15 -0
- package/scss/config/mixins/_kbd.scss +22 -0
- package/scss/config/mixins/_layout.scss +117 -0
- package/scss/config/mixins/_link.scss +55 -0
- package/scss/config/mixins/_ln-table.scss +420 -0
- package/scss/config/mixins/_loader.scss +26 -0
- package/scss/config/mixins/_modal.scss +66 -0
- package/scss/config/mixins/_motion.scss +19 -0
- package/scss/config/mixins/_nav.scss +273 -0
- package/scss/config/mixins/_page-header.scss +69 -0
- package/scss/config/mixins/_popover.scss +25 -0
- package/scss/config/mixins/_position.scss +32 -0
- package/scss/config/mixins/_progress.scss +56 -0
- package/scss/config/mixins/_prose.scss +127 -0
- package/scss/config/mixins/_shadows.scss +8 -0
- package/scss/config/mixins/_sidebar.scss +95 -0
- package/scss/config/mixins/_sizing.scss +6 -0
- package/scss/config/mixins/_spacing.scss +19 -0
- package/scss/config/mixins/_stat-card.scss +68 -0
- package/scss/config/mixins/_status-badge.scss +83 -0
- package/scss/config/mixins/_stepper.scss +78 -0
- package/scss/config/mixins/_table.scss +215 -0
- package/scss/config/mixins/_tabs.scss +64 -0
- package/scss/config/mixins/_timeline.scss +69 -0
- package/scss/config/mixins/_toast.scss +148 -0
- package/scss/config/mixins/_tooltip.scss +111 -0
- package/scss/config/mixins/_transitions.scss +10 -0
- package/scss/config/mixins/_translations.scss +124 -0
- package/scss/config/mixins/_typography.scss +57 -0
- package/scss/config/mixins/_upload.scss +168 -0
- package/scss/ln-ashlar.scss +62 -0
- package/scss/tabler-icons.txt +5039 -0
- package/scss/utilities/_animations.scss +83 -0
- package/scss/utilities/_utilities.scss +49 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# ln-toast
|
|
2
|
+
|
|
3
|
+
> Service-style non-blocking status notifications, managed reactively via window events.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Philosophy & The Toast Mindset
|
|
8
|
+
|
|
9
|
+
In `ln-ashlar`, the core design principle is **decoupling**. Toasts are a pure application-level service.
|
|
10
|
+
|
|
11
|
+
1. **Zero-Markup Dispatch (JavaScript)**: Components and pages never import toast files or call imperative layout methods directly. To show a notification, any script or controller simply dispatches an `ln-toast:enqueue` event to the global `window` object.
|
|
12
|
+
2. **Central Coordination (The Service)**: A single viewport container (`[data-ln-toast]`) listens for these window-level events, builds card elements dynamically from templates, coordinates automatic 6-second exit timers, pauses timers on mouse hover, and destroys elements after animations.
|
|
13
|
+
3. **Decoupled Styling (CSS)**: Visual alert chrome, slide-in animations, and side-accent status colors are handled in Vanilla CSS. The library ships mixins like `@include toast-container` and `@include toast-card` for styling.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. Minimal Blueprint
|
|
18
|
+
|
|
19
|
+
To enable toasts, simply place a single container `<ul>` in your HTML layout (typically right before the closing `</body>` tag).
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<!-- The Central Toast Container -->
|
|
23
|
+
<ul data-ln-toast data-ln-toast-timeout="6000" data-ln-toast-max="5"></ul>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Key Anatomy Rules
|
|
27
|
+
- **The Container (`data-ln-toast`)**: Creates the toast listener service.
|
|
28
|
+
- **Auto-Dismiss Timeout (`data-ln-toast-timeout="6000"`)**: Default dismissal duration in milliseconds. Use `0` for persistent notifications.
|
|
29
|
+
- **Max Stack size (`data-ln-toast-max="5"`)**: Evicts the oldest toast when the count exceeds the threshold.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 3. The Window API Contract
|
|
34
|
+
|
|
35
|
+
Toasts are triggered exclusively by dispatching CustomEvents on the global `window` object. **The window event is the sole contract.**
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// Dispatch a success notification
|
|
39
|
+
window.dispatchEvent(new CustomEvent('ln-toast:enqueue', {
|
|
40
|
+
detail: {
|
|
41
|
+
type: 'success',
|
|
42
|
+
title: 'Saved',
|
|
43
|
+
message: 'Changes have been saved successfully.'
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Enqueue Event Options (`ln-toast:enqueue`)
|
|
49
|
+
Pass options inside the event's `detail` object:
|
|
50
|
+
|
|
51
|
+
| Field | Type | Description |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `type` | `success\|error\|warn\|info` | Toast category. Drives accents, aria-live roles, and default titles. |
|
|
54
|
+
| `title` | string | Optional card title. |
|
|
55
|
+
| `message` | string \| string[] | Card body content. Pass an array of strings to render a bulleted validation error list. |
|
|
56
|
+
| `timeout` | number | Dismissal timeout in ms. Use `0` to make it persistent. |
|
|
57
|
+
|
|
58
|
+
### Clear Event (`ln-toast:clear`)
|
|
59
|
+
Dismiss all active toasts in the viewport:
|
|
60
|
+
```js
|
|
61
|
+
window.dispatchEvent(new CustomEvent('ln-toast:clear'));
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 4. Toast Types & Default Accessibility
|
|
67
|
+
|
|
68
|
+
ARIA roles and accessibility live-regions are injected automatically by the component depending on the category:
|
|
69
|
+
|
|
70
|
+
| Category | Default Title | `aria-live` | `role` |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| `success` | Success | `polite` | `status` |
|
|
73
|
+
| `error` | Error | `assertive` | `alert` |
|
|
74
|
+
| `warn` | Warning | `polite` | `status` |
|
|
75
|
+
| `info` | Information | `polite` | `status` |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 5. Integration Patterns
|
|
80
|
+
|
|
81
|
+
### A. SSR-Rendered Flash Messages (Hydration)
|
|
82
|
+
For server-side frameworks (like Laravel, Rails, or ASP.NET), you can place initial toast cards inside the container. The component will hydrate and auto-dismiss them on load:
|
|
83
|
+
```html
|
|
84
|
+
<ul data-ln-toast>
|
|
85
|
+
<li data-ln-toast-item data-type="success" data-title="Saved">
|
|
86
|
+
Changes have been saved successfully.
|
|
87
|
+
</li>
|
|
88
|
+
</ul>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### B. Bulleted Validation Error Maps
|
|
92
|
+
To display a list of form validation errors inside a single toast, pass an array of strings to `message`:
|
|
93
|
+
```js
|
|
94
|
+
window.dispatchEvent(new CustomEvent('ln-toast:enqueue', {
|
|
95
|
+
detail: {
|
|
96
|
+
type: 'error',
|
|
97
|
+
title: 'Validation Failed',
|
|
98
|
+
message: ['Email is required.', 'Password is too short.']
|
|
99
|
+
}
|
|
100
|
+
}));
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 6. Integration & Source Files
|
|
106
|
+
|
|
107
|
+
- **Unified Bundle**: Loaded automatically with the main bundle:
|
|
108
|
+
```html
|
|
109
|
+
<script src="dist/ln-ashlar.iife.js" defer></script>
|
|
110
|
+
```
|
|
111
|
+
- **Standalone IIFE**: For lightweight pages, load the standalone, self-registering IIFE version:
|
|
112
|
+
```html
|
|
113
|
+
<script src="js/ln-toast/ln-toast.js" defer></script>
|
|
114
|
+
```
|
|
115
|
+
- **Active Source (ESM)**: Development source is located at [js/ln-toast/src/ln-toast.js](file:///c:/laragon/www/ln-ashlar/js/ln-toast/src/ln-toast.js).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Related
|
|
120
|
+
- **[`ln-modal`](../ln-modal/README.md)** — Viewport-blocking focus-gated dialogs.
|
|
121
|
+
- **[`ln-confirm`](../ln-confirm/README.md)** — Lightweight inline action confirmations.
|
|
122
|
+
- **Architecture deep-dive** — [`docs/js/toast.md`](../../docs/js/toast.md).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
(function(){"use strict";const A={};function q(o,r){A[o]||(A[o]=document.querySelector('[data-ln-template="'+o+'"]'));const u=A[o];return u?u.content.cloneNode(!0):(console.warn("["+r+'] Template "'+o+'" not found'),null)}function M(o,r){if(!o||!r)return o;const u=o.querySelectorAll("[data-ln-field]");for(let i=0;i<u.length;i++){const a=u[i],c=a.getAttribute("data-ln-field");r[c]!=null&&(a.textContent=r[c])}const _=o.querySelectorAll("[data-ln-attr]");for(let i=0;i<_.length;i++){const a=_[i],c=a.getAttribute("data-ln-attr").split(",");for(let f=0;f<c.length;f++){const m=c[f].trim().split(":");if(m.length!==2)continue;const y=m[0].trim(),d=m[1].trim();r[d]!=null&&a.setAttribute(y,r[d])}}const g=o.querySelectorAll("[data-ln-show]");for(let i=0;i<g.length;i++){const a=g[i],c=a.getAttribute("data-ln-show");c in r&&a.classList.toggle("hidden",!r[c])}const h=o.querySelectorAll("[data-ln-class]");for(let i=0;i<h.length;i++){const a=h[i],c=a.getAttribute("data-ln-class").split(",");for(let f=0;f<c.length;f++){const m=c[f].trim().split(":");if(m.length!==2)continue;const y=m[0].trim(),d=m[1].trim();d in r&&a.classList.toggle(y,!!r[d])}}return o}function b(o,r){if(!document.body){document.addEventListener("DOMContentLoaded",function(){b(o,r)}),console.warn("["+r+'] Script loaded before <body> — add "defer" to your <script> tag');return}o()}function x(o,r,u){if(o){const _=o.querySelector('[data-ln-template="'+r+'"]');if(_)return _.content.cloneNode(!0)}return q(r,u)}const w={};function N(o,r){w[o]=r}function D(o){return w[o]||{ingress:r=>r,egress:r=>r}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=N,window.lnCore.getDataMapper=D);const I=`<li class="ln-toast__item">\r
|
|
2
|
+
<div class="ln-toast__card" data-ln-attr="role:role, aria-live:ariaLive">\r
|
|
3
|
+
<div class="ln-toast__side">\r
|
|
4
|
+
<svg class="ln-icon" aria-hidden="true"><use href=""></use></svg>\r
|
|
5
|
+
</div>\r
|
|
6
|
+
<div class="ln-toast__content">\r
|
|
7
|
+
<div class="ln-toast__head">\r
|
|
8
|
+
<strong class="ln-toast__title" data-ln-field="title"></strong>\r
|
|
9
|
+
</div>\r
|
|
10
|
+
<button type="button" class="ln-toast__close" aria-label="Close"><svg class="ln-icon" aria-hidden="true"><use href="#ln-x"></use></svg></button>\r
|
|
11
|
+
<div class="ln-toast__body" data-ln-show="hasBody"></div>\r
|
|
12
|
+
</div>\r
|
|
13
|
+
</div>\r
|
|
14
|
+
</li>\r
|
|
15
|
+
`;(function(){const o="data-ln-toast",r="lnToast",u="ln-toast-item",_={success:"circle-check",error:"circle-x",warn:"alert-triangle",info:"info-circle"},g={success:"success",error:"error",warn:"warning",info:"info"},h={success:"Success",error:"Error",warn:"Warning",info:"Information"};if(window.__lnToastLoaded)return;window.__lnToastLoaded=!0;function i(){if(document.querySelector('[data-ln-template="ln-toast-item"]')||!document.body)return;const t=document.createElement("template");t.setAttribute("data-ln-template","ln-toast-item"),t.innerHTML=I,document.body.appendChild(t)}function a(t){if(!t||t.nodeType!==1)return;const e=Array.from(t.querySelectorAll("["+o+"]"));t.hasAttribute&&t.hasAttribute(o)&&e.push(t);for(const n of e)n[r]||new c(n)}function c(t){this.dom=t,t[r]=this,this.timeoutDefault=parseInt(t.getAttribute("data-ln-toast-timeout")||"6000",10),this.max=parseInt(t.getAttribute("data-ln-toast-max")||"5",10);for(const e of Array.from(t.querySelectorAll("[data-ln-toast-item]")))B(e,t);return this}c.prototype.destroy=function(){if(this.dom[r]){for(const t of Array.from(this.dom.children))d(t);delete this.dom[r]}};function f(t,e){const n=((t.type||"info")+"").toLowerCase(),l=x(e,u,"ln-toast");if(!l)return console.warn('[ln-toast] Template "'+u+'" not found'),null;const s=l.firstElementChild;if(!s)return null;const p=!!(t.message||t.data&&t.data.errors);M(s,{title:t.title||h[n]||h.info,role:n==="error"?"alert":"status",ariaLive:n==="error"?"assertive":"polite",hasBody:p});const v=s.querySelector(".ln-toast__card");v&&v.classList.add(g[n]||"info");const T=s.querySelector(".ln-toast__side");if(T){const S=T.querySelector("use");S&&S.setAttribute("href","#ln-"+(_[n]||_.info))}const L=s.querySelector(".ln-toast__body");L&&p&&m(L,t);const E=s.querySelector(".ln-toast__close");return E&&E.addEventListener("click",function(){d(s)}),s}function m(t,e){if(e.message)if(Array.isArray(e.message)){const n=document.createElement("ul");for(const l of e.message){const s=document.createElement("li");s.textContent=l,n.appendChild(s)}t.appendChild(n)}else{const n=document.createElement("p");n.textContent=e.message,t.appendChild(n)}if(e.data&&e.data.errors){const n=document.createElement("ul");for(const l of Object.values(e.data.errors).flat()){const s=document.createElement("li");s.textContent=l,n.appendChild(s)}t.appendChild(n)}}function y(t,e){for(;t.dom.children.length>=t.max;)t.dom.removeChild(t.dom.firstElementChild);t.dom.appendChild(e),requestAnimationFrame(()=>e.classList.add("ln-toast__item--in"))}function d(t){!t||!t.parentNode||(clearTimeout(t._timer),t.classList.remove("ln-toast__item--in"),t.classList.add("ln-toast__item--out"),setTimeout(()=>{t.parentNode&&t.parentNode.removeChild(t)},200))}function C(t){let e=t&&t.container;return typeof e=="string"&&(e=document.querySelector(e)),e instanceof HTMLElement||(e=document.querySelector("["+o+"]")||document.getElementById("ln-toast-container")),e||null}function B(t,e){const n=((t.getAttribute("data-type")||"info")+"").toLowerCase(),l=t.getAttribute("data-title"),s=(t.innerText||t.textContent||"").trim(),p=f({type:n,title:l,message:s||void 0},e);p&&(t.parentNode&&t.parentNode.replaceChild(p,t),requestAnimationFrame(()=>p.classList.add("ln-toast__item--in")))}function O(t){const e=t.detail||{},n=C(e);if(!n){console.warn("[ln-toast] No toast container found");return}const l=n[r]||new c(n),s=f(e,n);if(!s)return;const p=Number.isFinite(e.timeout)?e.timeout:l.timeoutDefault;y(l,s),p>0&&(s._timer=setTimeout(()=>d(s),p))}function F(t){const e=t&&t.detail||{};if(e.container){const n=C(e);if(n)for(const l of Array.from(n.children))d(l)}else{const n=document.querySelectorAll("["+o+"]");for(const l of Array.from(n))for(const s of Array.from(l.children))d(s)}}b(function(){i(),window.addEventListener("ln-toast:enqueue",O),window.addEventListener("ln-toast:clear",F),new MutationObserver(function(e){for(const n of e){if(n.type==="attributes"){a(n.target);continue}for(const l of n.addedNodes)a(l)}}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[o]}),a(document.body)},"ln-toast")})()})();
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/* Live Networks — ln-toast (side-accent with icons) */
|
|
2
|
+
import { guardBody, cloneTemplateScoped, fill } from '../../ln-core';
|
|
3
|
+
import DEFAULT_TEMPLATE_HTML from '../template.html?raw';
|
|
4
|
+
|
|
5
|
+
(function () {
|
|
6
|
+
const DOM_SELECTOR = "data-ln-toast";
|
|
7
|
+
const DOM_ATTRIBUTE = "lnToast";
|
|
8
|
+
const TEMPLATE_NAME = "ln-toast-item";
|
|
9
|
+
|
|
10
|
+
const TYPE_ICON = { success: 'circle-check', error: 'circle-x', warn: 'alert-triangle', info: 'info-circle' };
|
|
11
|
+
|
|
12
|
+
const STATUS_CLASS = { success: 'success', error: 'error', warn: 'warning', info: 'info' };
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TITLES = { success: 'Success', error: 'Error', warn: 'Warning', info: 'Information' };
|
|
15
|
+
|
|
16
|
+
if (window.__lnToastLoaded) return;
|
|
17
|
+
window.__lnToastLoaded = true;
|
|
18
|
+
|
|
19
|
+
function _ensureTemplate() {
|
|
20
|
+
if (document.querySelector('[data-ln-template="ln-toast-item"]')) return;
|
|
21
|
+
if (!document.body) return;
|
|
22
|
+
const tmpl = document.createElement('template');
|
|
23
|
+
// Trust boundary: DEFAULT_TEMPLATE_HTML is a hardcoded library constant resolved at build time via ?raw import.
|
|
24
|
+
tmpl.setAttribute('data-ln-template', 'ln-toast-item');
|
|
25
|
+
tmpl.innerHTML = DEFAULT_TEMPLATE_HTML;
|
|
26
|
+
document.body.appendChild(tmpl);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _findContainers(root) {
|
|
30
|
+
if (!root || root.nodeType !== 1) return;
|
|
31
|
+
const items = Array.from(root.querySelectorAll("[" + DOM_SELECTOR + "]"));
|
|
32
|
+
if (root.hasAttribute && root.hasAttribute(DOM_SELECTOR)) items.push(root);
|
|
33
|
+
for (const el of items) {
|
|
34
|
+
if (!el[DOM_ATTRIBUTE]) new _Component(el);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _Component(dom) {
|
|
39
|
+
this.dom = dom;
|
|
40
|
+
dom[DOM_ATTRIBUTE] = this;
|
|
41
|
+
this.timeoutDefault = parseInt(dom.getAttribute("data-ln-toast-timeout") || "6000", 10);
|
|
42
|
+
this.max = parseInt(dom.getAttribute("data-ln-toast-max") || "5", 10);
|
|
43
|
+
|
|
44
|
+
for (const li of Array.from(dom.querySelectorAll("[data-ln-toast-item]"))) {
|
|
45
|
+
_hydrateLI(li, dom);
|
|
46
|
+
}
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_Component.prototype.destroy = function () {
|
|
51
|
+
if (!this.dom[DOM_ATTRIBUTE]) return;
|
|
52
|
+
for (const li of Array.from(this.dom.children)) {
|
|
53
|
+
_dismiss(li);
|
|
54
|
+
}
|
|
55
|
+
delete this.dom[DOM_ATTRIBUTE];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function _buildItem(opts, container) {
|
|
59
|
+
const type = ((opts.type || "info") + "").toLowerCase();
|
|
60
|
+
const fragment = cloneTemplateScoped(container, TEMPLATE_NAME, 'ln-toast');
|
|
61
|
+
if (!fragment) {
|
|
62
|
+
console.warn('[ln-toast] Template "' + TEMPLATE_NAME + '" not found');
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const li = fragment.firstElementChild;
|
|
66
|
+
if (!li) return null;
|
|
67
|
+
|
|
68
|
+
const hasBody = !!(opts.message || (opts.data && opts.data.errors));
|
|
69
|
+
|
|
70
|
+
fill(li, {
|
|
71
|
+
title: opts.title || DEFAULT_TITLES[type] || DEFAULT_TITLES.info,
|
|
72
|
+
role: type === 'error' ? 'alert' : 'status',
|
|
73
|
+
ariaLive: type === 'error' ? 'assertive' : 'polite',
|
|
74
|
+
hasBody: hasBody
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const card = li.querySelector('.ln-toast__card');
|
|
78
|
+
if (card) card.classList.add(STATUS_CLASS[type] || 'info');
|
|
79
|
+
|
|
80
|
+
const side = li.querySelector('.ln-toast__side');
|
|
81
|
+
if (side) {
|
|
82
|
+
const useEl = side.querySelector('use');
|
|
83
|
+
if (useEl) useEl.setAttribute('href', '#ln-' + (TYPE_ICON[type] || TYPE_ICON.info));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const bodyEl = li.querySelector('.ln-toast__body');
|
|
87
|
+
if (bodyEl && hasBody) _renderBody(bodyEl, opts);
|
|
88
|
+
|
|
89
|
+
const closeBtn = li.querySelector('.ln-toast__close');
|
|
90
|
+
if (closeBtn) closeBtn.addEventListener('click', function () { _dismiss(li); });
|
|
91
|
+
|
|
92
|
+
return li;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _renderBody(bodyEl, opts) {
|
|
96
|
+
if (opts.message) {
|
|
97
|
+
if (Array.isArray(opts.message)) {
|
|
98
|
+
const ul = document.createElement("ul");
|
|
99
|
+
for (const msg of opts.message) {
|
|
100
|
+
const lie = document.createElement("li");
|
|
101
|
+
lie.textContent = msg;
|
|
102
|
+
ul.appendChild(lie);
|
|
103
|
+
}
|
|
104
|
+
bodyEl.appendChild(ul);
|
|
105
|
+
} else {
|
|
106
|
+
const p = document.createElement("p");
|
|
107
|
+
p.textContent = opts.message;
|
|
108
|
+
bodyEl.appendChild(p);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (opts.data && opts.data.errors) {
|
|
112
|
+
const ul = document.createElement("ul");
|
|
113
|
+
for (const err of Object.values(opts.data.errors).flat()) {
|
|
114
|
+
const lie = document.createElement("li");
|
|
115
|
+
lie.textContent = err;
|
|
116
|
+
ul.appendChild(lie);
|
|
117
|
+
}
|
|
118
|
+
bodyEl.appendChild(ul);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _append(cmp, li) {
|
|
123
|
+
while (cmp.dom.children.length >= cmp.max) cmp.dom.removeChild(cmp.dom.firstElementChild);
|
|
124
|
+
cmp.dom.appendChild(li);
|
|
125
|
+
requestAnimationFrame(() => li.classList.add("ln-toast__item--in"));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _dismiss(li) {
|
|
129
|
+
if (!li || !li.parentNode) return;
|
|
130
|
+
clearTimeout(li._timer);
|
|
131
|
+
li.classList.remove("ln-toast__item--in");
|
|
132
|
+
li.classList.add("ln-toast__item--out");
|
|
133
|
+
setTimeout(() => { li.parentNode && li.parentNode.removeChild(li); }, 200);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function _resolveContainer(detail) {
|
|
137
|
+
let container = detail && detail.container;
|
|
138
|
+
if (typeof container === "string") container = document.querySelector(container);
|
|
139
|
+
if (!(container instanceof HTMLElement)) {
|
|
140
|
+
container = document.querySelector("[" + DOM_SELECTOR + "]") || document.getElementById("ln-toast-container");
|
|
141
|
+
}
|
|
142
|
+
return container || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _hydrateLI(li, container) {
|
|
146
|
+
const type = ((li.getAttribute("data-type") || "info") + "").toLowerCase();
|
|
147
|
+
const titleA = li.getAttribute("data-title");
|
|
148
|
+
const msgText = (li.innerText || li.textContent || "").trim();
|
|
149
|
+
|
|
150
|
+
const built = _buildItem({
|
|
151
|
+
type: type,
|
|
152
|
+
title: titleA,
|
|
153
|
+
message: msgText || undefined
|
|
154
|
+
}, container);
|
|
155
|
+
|
|
156
|
+
if (!built) return;
|
|
157
|
+
|
|
158
|
+
li.parentNode && li.parentNode.replaceChild(built, li);
|
|
159
|
+
requestAnimationFrame(() => built.classList.add("ln-toast__item--in"));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function _onEnqueue(e) {
|
|
163
|
+
const detail = e.detail || {};
|
|
164
|
+
const container = _resolveContainer(detail);
|
|
165
|
+
if (!container) {
|
|
166
|
+
console.warn('[ln-toast] No toast container found');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const cmp = container[DOM_ATTRIBUTE] || new _Component(container);
|
|
170
|
+
const li = _buildItem(detail, container);
|
|
171
|
+
if (!li) return;
|
|
172
|
+
const timeout = Number.isFinite(detail.timeout) ? detail.timeout : cmp.timeoutDefault;
|
|
173
|
+
_append(cmp, li);
|
|
174
|
+
if (timeout > 0) li._timer = setTimeout(() => _dismiss(li), timeout);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _onClear(e) {
|
|
178
|
+
const detail = (e && e.detail) || {};
|
|
179
|
+
if (detail.container) {
|
|
180
|
+
const el = _resolveContainer(detail);
|
|
181
|
+
if (el) {
|
|
182
|
+
for (const child of Array.from(el.children)) _dismiss(child);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
const containers = document.querySelectorAll("[" + DOM_SELECTOR + "]");
|
|
186
|
+
for (const el of Array.from(containers)) {
|
|
187
|
+
for (const child of Array.from(el.children)) _dismiss(child);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
guardBody(function () {
|
|
193
|
+
_ensureTemplate();
|
|
194
|
+
|
|
195
|
+
window.addEventListener('ln-toast:enqueue', _onEnqueue);
|
|
196
|
+
window.addEventListener('ln-toast:clear', _onClear);
|
|
197
|
+
|
|
198
|
+
const observer = new MutationObserver(function (muts) {
|
|
199
|
+
for (const m of muts) {
|
|
200
|
+
if (m.type === 'attributes') { _findContainers(m.target); continue; }
|
|
201
|
+
for (const n of m.addedNodes) {
|
|
202
|
+
_findContainers(n);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [DOM_SELECTOR] });
|
|
207
|
+
|
|
208
|
+
_findContainers(document.body);
|
|
209
|
+
}, 'ln-toast');
|
|
210
|
+
})();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<li class="ln-toast__item">
|
|
2
|
+
<div class="ln-toast__card" data-ln-attr="role:role, aria-live:ariaLive">
|
|
3
|
+
<div class="ln-toast__side">
|
|
4
|
+
<svg class="ln-icon" aria-hidden="true"><use href=""></use></svg>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="ln-toast__content">
|
|
7
|
+
<div class="ln-toast__head">
|
|
8
|
+
<strong class="ln-toast__title" data-ln-field="title"></strong>
|
|
9
|
+
</div>
|
|
10
|
+
<button type="button" class="ln-toast__close" aria-label="Close"><svg class="ln-icon" aria-hidden="true"><use href="#ln-x"></use></svg></button>
|
|
11
|
+
<div class="ln-toast__body" data-ln-show="hasBody"></div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</li>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# ln-toggle
|
|
2
|
+
|
|
3
|
+
> The smallest reactive primitive in `ln-ashlar` — a highly-specialized binary state machine.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Philosophy & The Primitive Mindset
|
|
8
|
+
|
|
9
|
+
In `ln-ashlar`, the core design principle is **orthogonality**. Rather than creating heavy components that mix state, visual presentation, and layout, we separate them into isolated concerns:
|
|
10
|
+
|
|
11
|
+
1. **The State Machine (JavaScript)**: The `ln-toggle` component (145 lines) only manages binary `open` / `close` state in the DOM and synchronizes ARIA accessibility. It does not own animations or visual geometries.
|
|
12
|
+
2. **The Visual Presentation (CSS)**: Visual transitions are handled in Vanilla CSS. The component simply toggles the `.open` class on the panel. CSS reads this class and runs transitions (e.g. height collapse or sliding drawers).
|
|
13
|
+
3. **Decoupled Binding (HTML)**: Triggers and panels are matched purely by ID. They can live anywhere in the DOM. Multiple triggers pointing to a single panel are supported natively, and all triggers stay perfectly synchronized.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. Minimal Blueprint
|
|
18
|
+
|
|
19
|
+
Triggers and panels are bound via ID. A panel must have a unique `id` and the `data-ln-toggle` attribute.
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<!-- Trigger anywhere -->
|
|
23
|
+
<button data-ln-toggle-for="example-panel">Toggle Options</button>
|
|
24
|
+
|
|
25
|
+
<!-- Panel anywhere -->
|
|
26
|
+
<section id="example-panel" data-ln-toggle class="collapsible">
|
|
27
|
+
<article class="collapsible-body">
|
|
28
|
+
<p>This is smooth collapsible content.</p>
|
|
29
|
+
</article>
|
|
30
|
+
</section>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Key Anatomy Rules
|
|
34
|
+
- **The Panel (`data-ln-toggle`)**: The value `open` represents open; anything else (empty or `close`) represents closed.
|
|
35
|
+
- **The Trigger (`data-ln-toggle-for="id"`)**: Automatically intercepts clicks to toggle the panel.
|
|
36
|
+
- **The Close Trigger (`data-ln-toggle-action="close"`)**: Forces the trigger to only close the panel (e.g. an "X" button inside a sidebar drawer).
|
|
37
|
+
- **The Body (`.collapsible-body`)**: Holds all padding and margins. The parent `.collapsible` container must have zero padding so it can transition to exactly `0px` height cleanly.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. The Declarative API & State Contract
|
|
42
|
+
|
|
43
|
+
There are no imperative JavaScript methods (like `open()` or `close()`) on the component instance. **The HTML attribute is the sole contract.**
|
|
44
|
+
|
|
45
|
+
Triggers, sibling components, external scripts, and manual DevTools edits all change state by writing the attribute:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const panel = document.getElementById('example-panel');
|
|
49
|
+
|
|
50
|
+
// Open the panel
|
|
51
|
+
panel.setAttribute('data-ln-toggle', 'open');
|
|
52
|
+
|
|
53
|
+
// Close the panel
|
|
54
|
+
panel.setAttribute('data-ln-toggle', 'close');
|
|
55
|
+
|
|
56
|
+
// Read-only state query
|
|
57
|
+
panel.lnToggle.isOpen; // Returns true/false
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Attributes
|
|
61
|
+
- `data-ln-toggle`: Placed on the panel to create the toggle instance.
|
|
62
|
+
- `data-ln-toggle-for`: Placed on triggers referencing the panel ID.
|
|
63
|
+
- `data-ln-toggle-action="open|close"`: Forces a trigger button to only open or only close the target.
|
|
64
|
+
- `data-ln-persist`: Saves the panel state individually in `localStorage`.
|
|
65
|
+
- storage key: `ln:toggle:{pagePath}:{id}`. Same IDs on different pages store separately.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 4. Transition Events
|
|
70
|
+
|
|
71
|
+
All events bubble. `event.detail.target` is always the panel element.
|
|
72
|
+
|
|
73
|
+
| Event | Cancelable | Dispatched When |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| **`ln-toggle:before-open`** | **Yes** | After attribute flips to `"open"`, before transition starts. Calling `event.preventDefault()` cancels the transition and reverts the attribute. |
|
|
76
|
+
| **`ln-toggle:open`** | No | After panel is fully open, classes added, and ARIA synced. |
|
|
77
|
+
| **`ln-toggle:before-close`** | **Yes** | After attribute flips to `"close"`, before transition starts. Calling `event.preventDefault()` cancels the close and reverts the attribute. |
|
|
78
|
+
| **`ln-toggle:close`** | No | After panel is fully closed, classes removed, and ARIA synced. |
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
// Example: Cancel open transition for unauthorized users
|
|
82
|
+
document.addEventListener('ln-toggle:before-open', (e) => {
|
|
83
|
+
if (e.detail.target.id === 'secure-panel' && !currentUser.isAdmin) {
|
|
84
|
+
e.preventDefault(); // Reverts attribute back to "close"
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 5. Integration Patterns
|
|
92
|
+
|
|
93
|
+
### A. Sidebar Drawer
|
|
94
|
+
Combine the panel with the library's `@mixin sidebar-drawer` and add a close button inside the sidebar:
|
|
95
|
+
```html
|
|
96
|
+
<aside id="menu" data-ln-toggle data-ln-persist class="sidebar">
|
|
97
|
+
<button data-ln-toggle-for="menu" data-ln-toggle-action="close">×</button>
|
|
98
|
+
</aside>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### B. Smooth Height Collapsible
|
|
102
|
+
Combine the panel with the library's `@mixin collapsible` and `.collapsible-body` wrapper to animate height cleanly:
|
|
103
|
+
```html
|
|
104
|
+
<section id="panel" data-ln-toggle class="collapsible">
|
|
105
|
+
<div class="collapsible-body">Content goes here...</div>
|
|
106
|
+
</section>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### C. Dismissible Alert
|
|
110
|
+
Combine the alert card with `data-ln-persist` so that once the user closes the alert, it stays closed across page reloads:
|
|
111
|
+
```html
|
|
112
|
+
<div class="alert" id="promo-banner" data-ln-toggle="open" data-ln-persist>
|
|
113
|
+
<p>Promo code active!</p>
|
|
114
|
+
<button data-ln-toggle-for="promo-banner" data-ln-toggle-action="close">×</button>
|
|
115
|
+
</div>
|
|
116
|
+
```
|
|
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-toggle/ln-toggle.js" defer></script>
|
|
129
|
+
```
|
|
130
|
+
- **Active Source (ESM)**: Development source is located at [js/ln-toggle/src/ln-toggle.js](file:///c:/laragon/www/ln-ashlar/js/ln-toggle/src/ln-toggle.js).
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Related
|
|
135
|
+
- **[`ln-accordion`](../ln-accordion/README.md)** — Single-open coordinator for toggle panels.
|
|
136
|
+
- **[`ln-dropdown`](../ln-dropdown/README.md)** — Menu wrapper adding click-outside/teleportation.
|
|
137
|
+
- **Architecture deep-dive** — [`docs/js/toggle.md`](../../docs/js/toggle.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";function h(n,t,o){n.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:o||{}}))}function A(n,t,o){const i=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:o||{}});return n.dispatchEvent(i),i}function m(n,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){m(n,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}n()}function y(n,t,o,i){if(n.nodeType!==1)return;const b=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",e=Array.from(n.querySelectorAll(b));n.matches&&n.matches(b)&&e.push(n);for(const r of e)r[o]||(r[o]=new i(r))}function E(n,t,o,i,g={}){const b=g.extraAttributes||[],e=g.onAttributeChange||null,r=g.onInit||null;function s(a){const l=a||document.body;y(l,n,t,o),r&&r(l)}return m(function(){const a=new MutationObserver(function(f){for(let u=0;u<f.length;u++){const c=f[u];if(c.type==="childList")for(let d=0;d<c.addedNodes.length;d++){const p=c.addedNodes[d];p.nodeType===1&&(y(p,n,t,o),r&&r(p))}else c.type==="attributes"&&(e&&c.target[t]?e(c.target,c.attributeName):(y(c.target,n,t,o),r&&r(c.target)))}});let l=[];if(n.indexOf("[")!==-1){const f=/\[([\w-]+)/g;let u;for(;(u=f.exec(n))!==null;)l.push(u[1])}else l.push(n);a.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:l.concat(b)})},i),window[t]=s,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){s(document.body)}):s(document.body),s}const w={};function C(n,t){w[n]=t}function L(n){return w[n]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=C,window.lnCore.getDataMapper=L);const T="ln:";function x(){return location.pathname.replace(/\/+$/,"").toLowerCase()||"/"}function v(n,t){const o=t.getAttribute("data-ln-persist"),i=o!==null&&o!==""?o:t.id;return i?T+n+":"+x()+":"+i:(console.warn('[ln-persist] Element requires id or data-ln-persist="key"',t),null)}function S(n,t){const o=v(n,t);if(!o)return null;try{const i=localStorage.getItem(o);return i!==null?JSON.parse(i):null}catch{return null}}function O(n,t,o){const i=v(n,t);if(i)try{localStorage.setItem(i,JSON.stringify(o))}catch{}}(function(){const n="data-ln-toggle",t="lnToggle";if(window[t]!==void 0)return;function o(e){const r=Array.from(e.querySelectorAll("[data-ln-toggle-for]"));e.hasAttribute&&e.hasAttribute("data-ln-toggle-for")&&r.push(e);for(const s of r){if(s[t+"Trigger"])continue;const a=function(u){if(u.ctrlKey||u.metaKey||u.button===1)return;u.preventDefault();const c=s.getAttribute("data-ln-toggle-for"),d=document.getElementById(c);if(!d||!d[t])return;const p=s.getAttribute("data-ln-toggle-action")||"toggle";if(p==="open")d.setAttribute(n,"open");else if(p==="close")d.setAttribute(n,"close");else if(p==="toggle"){const I=d.getAttribute(n);d.setAttribute(n,I==="open"?"close":"open")}};s.addEventListener("click",a),s[t+"Trigger"]=a;const l=s.getAttribute("data-ln-toggle-for"),f=document.getElementById(l);f&&f[t]&&s.setAttribute("aria-expanded",f[t].isOpen?"true":"false")}}function i(e,r){const s=document.querySelectorAll('[data-ln-toggle-for="'+e.id+'"]');for(const a of s)a.setAttribute("aria-expanded",r?"true":"false")}function g(e){if(this.dom=e,e.hasAttribute("data-ln-persist")){const r=S("toggle",e);r!==null&&e.setAttribute(n,r)}return this.isOpen=e.getAttribute(n)==="open",this.isOpen&&e.classList.add("open"),i(e,this.isOpen),this}g.prototype.destroy=function(){if(!this.dom[t])return;h(this.dom,"ln-toggle:destroyed",{target:this.dom});const e=document.querySelectorAll('[data-ln-toggle-for="'+this.dom.id+'"]');for(const r of e)r[t+"Trigger"]&&(r.removeEventListener("click",r[t+"Trigger"]),delete r[t+"Trigger"]);delete this.dom[t]};function b(e){const r=e[t];if(!r)return;const a=e.getAttribute(n)==="open";if(a!==r.isOpen)if(a){if(A(e,"ln-toggle:before-open",{target:e}).defaultPrevented){e.setAttribute(n,"close");return}r.isOpen=!0,e.classList.add("open"),i(e,!0),h(e,"ln-toggle:open",{target:e}),e.hasAttribute("data-ln-persist")&&O("toggle",e,"open")}else{if(A(e,"ln-toggle:before-close",{target:e}).defaultPrevented){e.setAttribute(n,"open");return}r.isOpen=!1,e.classList.remove("open"),i(e,!1),h(e,"ln-toggle:close",{target:e}),e.hasAttribute("data-ln-persist")&&O("toggle",e,"close")}}E(n,t,g,"ln-toggle",{extraAttributes:["data-ln-toggle-for"],onAttributeChange:b,onInit:o})})()})();
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { registerComponent, dispatch, dispatchCancelable } from '../../ln-core';
|
|
2
|
+
import { persistGet, persistSet } from '../../ln-core';
|
|
3
|
+
|
|
4
|
+
(function () {
|
|
5
|
+
const DOM_SELECTOR = 'data-ln-toggle';
|
|
6
|
+
const DOM_ATTRIBUTE = 'lnToggle';
|
|
7
|
+
|
|
8
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
function _attachTriggers(root) {
|
|
12
|
+
const triggers = Array.from(root.querySelectorAll('[data-ln-toggle-for]'));
|
|
13
|
+
if (root.hasAttribute && root.hasAttribute('data-ln-toggle-for')) {
|
|
14
|
+
triggers.push(root);
|
|
15
|
+
}
|
|
16
|
+
for (const btn of triggers) {
|
|
17
|
+
if (btn[DOM_ATTRIBUTE + 'Trigger']) continue;
|
|
18
|
+
const handler = function (e) {
|
|
19
|
+
if (e.ctrlKey || e.metaKey || e.button === 1) return;
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
const targetId = btn.getAttribute('data-ln-toggle-for');
|
|
22
|
+
const target = document.getElementById(targetId);
|
|
23
|
+
if (!target || !target[DOM_ATTRIBUTE]) return;
|
|
24
|
+
|
|
25
|
+
const action = btn.getAttribute('data-ln-toggle-action') || 'toggle';
|
|
26
|
+
if (action === 'open') {
|
|
27
|
+
target.setAttribute(DOM_SELECTOR, 'open');
|
|
28
|
+
} else if (action === 'close') {
|
|
29
|
+
target.setAttribute(DOM_SELECTOR, 'close');
|
|
30
|
+
} else if (action === 'toggle') {
|
|
31
|
+
const current = target.getAttribute(DOM_SELECTOR);
|
|
32
|
+
target.setAttribute(DOM_SELECTOR, current === 'open' ? 'close' : 'open');
|
|
33
|
+
}
|
|
34
|
+
// unknown action — silent no-op
|
|
35
|
+
};
|
|
36
|
+
btn.addEventListener('click', handler);
|
|
37
|
+
btn[DOM_ATTRIBUTE + 'Trigger'] = handler;
|
|
38
|
+
const targetId = btn.getAttribute('data-ln-toggle-for');
|
|
39
|
+
const target = document.getElementById(targetId);
|
|
40
|
+
if (target && target[DOM_ATTRIBUTE]) {
|
|
41
|
+
btn.setAttribute('aria-expanded', target[DOM_ATTRIBUTE].isOpen ? 'true' : 'false');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _syncTriggerAria(panelEl, isOpen) {
|
|
47
|
+
const triggers = document.querySelectorAll(
|
|
48
|
+
'[data-ln-toggle-for="' + panelEl.id + '"]'
|
|
49
|
+
);
|
|
50
|
+
for (const trigger of triggers) {
|
|
51
|
+
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Component ─────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function _component(dom) {
|
|
58
|
+
this.dom = dom;
|
|
59
|
+
|
|
60
|
+
// ─── Restore persisted state ──────────────────────────────
|
|
61
|
+
if (dom.hasAttribute('data-ln-persist')) {
|
|
62
|
+
const saved = persistGet('toggle', dom);
|
|
63
|
+
if (saved !== null) {
|
|
64
|
+
dom.setAttribute(DOM_SELECTOR, saved);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.isOpen = dom.getAttribute(DOM_SELECTOR) === 'open';
|
|
69
|
+
|
|
70
|
+
if (this.isOpen) {
|
|
71
|
+
dom.classList.add('open');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_syncTriggerAria(dom, this.isOpen);
|
|
75
|
+
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_component.prototype.destroy = function () {
|
|
80
|
+
if (!this.dom[DOM_ATTRIBUTE]) return;
|
|
81
|
+
dispatch(this.dom, 'ln-toggle:destroyed', { target: this.dom });
|
|
82
|
+
const triggers = document.querySelectorAll('[data-ln-toggle-for="' + this.dom.id + '"]');
|
|
83
|
+
for (const btn of triggers) {
|
|
84
|
+
if (btn[DOM_ATTRIBUTE + 'Trigger']) {
|
|
85
|
+
btn.removeEventListener('click', btn[DOM_ATTRIBUTE + 'Trigger']);
|
|
86
|
+
delete btn[DOM_ATTRIBUTE + 'Trigger'];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
delete this.dom[DOM_ATTRIBUTE];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ─── Attribute Sync ────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function _syncAttribute(el) {
|
|
95
|
+
const instance = el[DOM_ATTRIBUTE];
|
|
96
|
+
if (!instance) return;
|
|
97
|
+
|
|
98
|
+
const value = el.getAttribute(DOM_SELECTOR);
|
|
99
|
+
const shouldBeOpen = value === 'open';
|
|
100
|
+
|
|
101
|
+
if (shouldBeOpen === instance.isOpen) return;
|
|
102
|
+
|
|
103
|
+
if (shouldBeOpen) {
|
|
104
|
+
const before = dispatchCancelable(el, 'ln-toggle:before-open', { target: el });
|
|
105
|
+
if (before.defaultPrevented) {
|
|
106
|
+
el.setAttribute(DOM_SELECTOR, 'close');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
instance.isOpen = true;
|
|
110
|
+
el.classList.add('open');
|
|
111
|
+
_syncTriggerAria(el, true);
|
|
112
|
+
dispatch(el, 'ln-toggle:open', { target: el });
|
|
113
|
+
if (el.hasAttribute('data-ln-persist')) {
|
|
114
|
+
persistSet('toggle', el, 'open');
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
const before = dispatchCancelable(el, 'ln-toggle:before-close', { target: el });
|
|
118
|
+
if (before.defaultPrevented) {
|
|
119
|
+
el.setAttribute(DOM_SELECTOR, 'open');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
instance.isOpen = false;
|
|
123
|
+
el.classList.remove('open');
|
|
124
|
+
_syncTriggerAria(el, false);
|
|
125
|
+
dispatch(el, 'ln-toggle:close', { target: el });
|
|
126
|
+
if (el.hasAttribute('data-ln-persist')) {
|
|
127
|
+
persistSet('toggle', el, 'close');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Init ──────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-toggle', {
|
|
135
|
+
extraAttributes: ['data-ln-toggle-for'],
|
|
136
|
+
onAttributeChange: _syncAttribute,
|
|
137
|
+
onInit: _attachTriggers
|
|
138
|
+
});
|
|
139
|
+
})();
|