@livenetworks/ashlar 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +177 -0
  2. package/js/COMPONENTS.md +1102 -0
  3. package/js/index.js +41 -0
  4. package/js/ln-accordion/README.md +137 -0
  5. package/js/ln-accordion/ln-accordion.js +1 -0
  6. package/js/ln-accordion/src/ln-accordion.js +41 -0
  7. package/js/ln-ajax/README.md +91 -0
  8. package/js/ln-ajax/ln-ajax.js +1 -0
  9. package/js/ln-ajax/src/ln-ajax.js +277 -0
  10. package/js/ln-api-connector/README.md +150 -0
  11. package/js/ln-api-connector/ln-api-connector.js +1 -0
  12. package/js/ln-api-connector/src/ln-api-connector.js +265 -0
  13. package/js/ln-autoresize/README.md +80 -0
  14. package/js/ln-autoresize/ln-autoresize.js +1 -0
  15. package/js/ln-autoresize/src/ln-autoresize.js +47 -0
  16. package/js/ln-autosave/README.md +92 -0
  17. package/js/ln-autosave/ln-autosave.js +1 -0
  18. package/js/ln-autosave/src/ln-autosave.js +147 -0
  19. package/js/ln-circular-progress/README.md +161 -0
  20. package/js/ln-circular-progress/ln-circular-progress.js +1 -0
  21. package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
  22. package/js/ln-confirm/README.md +86 -0
  23. package/js/ln-confirm/_ln-confirm.scss +13 -0
  24. package/js/ln-confirm/ln-confirm.js +1 -0
  25. package/js/ln-confirm/src/ln-confirm.js +131 -0
  26. package/js/ln-core/crypto.js +83 -0
  27. package/js/ln-core/helpers.js +411 -0
  28. package/js/ln-core/index.js +5 -0
  29. package/js/ln-core/persist.js +71 -0
  30. package/js/ln-core/positioning.js +207 -0
  31. package/js/ln-core/reactive.js +74 -0
  32. package/js/ln-couchdb-connector/README.md +156 -0
  33. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
  34. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
  35. package/js/ln-data-coordinator/README.md +165 -0
  36. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
  37. package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
  38. package/js/ln-data-store/README.md +94 -0
  39. package/js/ln-data-store/ln-data-store.js +1 -0
  40. package/js/ln-data-store/src/ln-data-store.js +699 -0
  41. package/js/ln-data-table/README.md +110 -0
  42. package/js/ln-data-table/ln-data-table.js +1 -0
  43. package/js/ln-data-table/ln-data-table.scss +10 -0
  44. package/js/ln-data-table/src/ln-data-table.js +1103 -0
  45. package/js/ln-date/README.md +151 -0
  46. package/js/ln-date/ln-date.js +1 -0
  47. package/js/ln-date/src/ln-date.js +442 -0
  48. package/js/ln-dropdown/README.md +117 -0
  49. package/js/ln-dropdown/ln-dropdown.js +1 -0
  50. package/js/ln-dropdown/ln-dropdown.scss +15 -0
  51. package/js/ln-dropdown/src/ln-dropdown.js +174 -0
  52. package/js/ln-external-links/README.md +341 -0
  53. package/js/ln-external-links/ln-external-links.js +1 -0
  54. package/js/ln-external-links/src/ln-external-links.js +116 -0
  55. package/js/ln-filter/README.md +99 -0
  56. package/js/ln-filter/ln-filter.js +1 -0
  57. package/js/ln-filter/ln-filter.scss +7 -0
  58. package/js/ln-filter/src/ln-filter.js +404 -0
  59. package/js/ln-form/README.md +101 -0
  60. package/js/ln-form/ln-form.js +1 -0
  61. package/js/ln-form/src/ln-form.js +199 -0
  62. package/js/ln-http/README.md +89 -0
  63. package/js/ln-http/ln-http.js +1 -0
  64. package/js/ln-http/src/ln-http.js +219 -0
  65. package/js/ln-icons/README.md +88 -0
  66. package/js/ln-icons/ln-icons.js +1 -0
  67. package/js/ln-icons/src/ln-icons.js +169 -0
  68. package/js/ln-link/README.md +303 -0
  69. package/js/ln-link/ln-link.js +1 -0
  70. package/js/ln-link/src/ln-link.js +196 -0
  71. package/js/ln-modal/README.md +154 -0
  72. package/js/ln-modal/ln-modal.js +1 -0
  73. package/js/ln-modal/ln-modal.scss +11 -0
  74. package/js/ln-modal/src/ln-modal.js +201 -0
  75. package/js/ln-nav/README.md +70 -0
  76. package/js/ln-nav/ln-nav.js +1 -0
  77. package/js/ln-nav/src/ln-nav.js +177 -0
  78. package/js/ln-number/README.md +122 -0
  79. package/js/ln-number/ln-number.js +1 -0
  80. package/js/ln-number/src/ln-number.js +302 -0
  81. package/js/ln-popover/README.md +127 -0
  82. package/js/ln-popover/ln-popover.js +1 -0
  83. package/js/ln-popover/src/ln-popover.js +288 -0
  84. package/js/ln-progress/README.md +442 -0
  85. package/js/ln-progress/ln-progress.js +1 -0
  86. package/js/ln-progress/src/ln-progress.js +150 -0
  87. package/js/ln-search/README.md +83 -0
  88. package/js/ln-search/ln-search.js +1 -0
  89. package/js/ln-search/ln-search.scss +7 -0
  90. package/js/ln-search/src/ln-search.js +114 -0
  91. package/js/ln-sortable/README.md +95 -0
  92. package/js/ln-sortable/ln-sortable.js +1 -0
  93. package/js/ln-sortable/src/ln-sortable.js +203 -0
  94. package/js/ln-table/README.md +101 -0
  95. package/js/ln-table/ln-table-sort.js +1 -0
  96. package/js/ln-table/ln-table.js +1 -0
  97. package/js/ln-table/ln-table.scss +11 -0
  98. package/js/ln-table/src/ln-table-sort.js +168 -0
  99. package/js/ln-table/src/ln-table.js +473 -0
  100. package/js/ln-tabs/README.md +137 -0
  101. package/js/ln-tabs/ln-tabs.js +1 -0
  102. package/js/ln-tabs/src/ln-tabs.js +171 -0
  103. package/js/ln-time/README.md +81 -0
  104. package/js/ln-time/ln-time.js +1 -0
  105. package/js/ln-time/src/ln-time.js +192 -0
  106. package/js/ln-toast/README.md +122 -0
  107. package/js/ln-toast/ln-toast.js +15 -0
  108. package/js/ln-toast/src/ln-toast.js +210 -0
  109. package/js/ln-toast/template.html +14 -0
  110. package/js/ln-toggle/README.md +137 -0
  111. package/js/ln-toggle/ln-toggle.js +1 -0
  112. package/js/ln-toggle/src/ln-toggle.js +139 -0
  113. package/js/ln-tooltip/README.md +58 -0
  114. package/js/ln-tooltip/ln-tooltip.js +1 -0
  115. package/js/ln-tooltip/ln-tooltip.scss +9 -0
  116. package/js/ln-tooltip/src/ln-tooltip.js +169 -0
  117. package/js/ln-translations/README.md +96 -0
  118. package/js/ln-translations/ln-translations.js +1 -0
  119. package/js/ln-translations/src/ln-translations.js +275 -0
  120. package/js/ln-upload/README.md +180 -0
  121. package/js/ln-upload/ln-upload.js +1 -0
  122. package/js/ln-upload/ln-upload.scss +20 -0
  123. package/js/ln-upload/src/ln-upload.js +407 -0
  124. package/js/ln-validate/README.md +108 -0
  125. package/js/ln-validate/ln-validate.js +1 -0
  126. package/js/ln-validate/src/ln-validate.js +160 -0
  127. package/package.json +55 -0
  128. package/scss/base/_global.scss +83 -0
  129. package/scss/base/_reset.scss +17 -0
  130. package/scss/base/_typography.scss +125 -0
  131. package/scss/components/_accordion.scss +34 -0
  132. package/scss/components/_ajax.scss +15 -0
  133. package/scss/components/_alert.scss +5 -0
  134. package/scss/components/_app-shell.scss +15 -0
  135. package/scss/components/_avatar.scss +6 -0
  136. package/scss/components/_breadcrumbs.scss +33 -0
  137. package/scss/components/_button.scss +20 -0
  138. package/scss/components/_card.scss +10 -0
  139. package/scss/components/_chip.scss +5 -0
  140. package/scss/components/_circular-progress.scss +29 -0
  141. package/scss/components/_confirm.scss +5 -0
  142. package/scss/components/_data-table.scss +83 -0
  143. package/scss/components/_dropdown.scss +25 -0
  144. package/scss/components/_empty-state.scss +22 -0
  145. package/scss/components/_form.scss +100 -0
  146. package/scss/components/_layout.scss +8 -0
  147. package/scss/components/_link.scss +11 -0
  148. package/scss/components/_ln-table.scss +60 -0
  149. package/scss/components/_loader.scss +6 -0
  150. package/scss/components/_modal.scss +20 -0
  151. package/scss/components/_nav.scss +9 -0
  152. package/scss/components/_page-header.scss +10 -0
  153. package/scss/components/_popover.scss +10 -0
  154. package/scss/components/_progress.scss +17 -0
  155. package/scss/components/_prose.scss +5 -0
  156. package/scss/components/_scrollbar.scss +32 -0
  157. package/scss/components/_sections.scss +12 -0
  158. package/scss/components/_sidebar.scss +5 -0
  159. package/scss/components/_stat-card.scss +5 -0
  160. package/scss/components/_status-badge.scss +4 -0
  161. package/scss/components/_stepper.scss +5 -0
  162. package/scss/components/_table.scss +19 -0
  163. package/scss/components/_tabs.scss +21 -0
  164. package/scss/components/_timeline.scss +14 -0
  165. package/scss/components/_toast.scss +41 -0
  166. package/scss/components/_toggle.scss +81 -0
  167. package/scss/components/_tooltip.scss +18 -0
  168. package/scss/components/_translations.scss +111 -0
  169. package/scss/components/_upload.scss +51 -0
  170. package/scss/config/_breakpoints.scss +72 -0
  171. package/scss/config/_density.scss +117 -0
  172. package/scss/config/_icons.scss +37 -0
  173. package/scss/config/_mixins.scss +13 -0
  174. package/scss/config/_theme.scss +216 -0
  175. package/scss/config/_tokens.scss +419 -0
  176. package/scss/config/mixins/_accordion.scss +52 -0
  177. package/scss/config/mixins/_ajax.scss +39 -0
  178. package/scss/config/mixins/_alert.scss +82 -0
  179. package/scss/config/mixins/_app-shell.scss +312 -0
  180. package/scss/config/mixins/_avatar.scss +109 -0
  181. package/scss/config/mixins/_borders.scss +36 -0
  182. package/scss/config/mixins/_breadcrumbs.scss +72 -0
  183. package/scss/config/mixins/_breakpoints.scss +62 -0
  184. package/scss/config/mixins/_btn.scss +179 -0
  185. package/scss/config/mixins/_card.scss +338 -0
  186. package/scss/config/mixins/_chip.scss +66 -0
  187. package/scss/config/mixins/_circular-progress.scss +71 -0
  188. package/scss/config/mixins/_collapsible.scss +24 -0
  189. package/scss/config/mixins/_colors.scss +46 -0
  190. package/scss/config/mixins/_confirm.scss +31 -0
  191. package/scss/config/mixins/_data-table.scss +346 -0
  192. package/scss/config/mixins/_display.scss +32 -0
  193. package/scss/config/mixins/_dropdown.scss +143 -0
  194. package/scss/config/mixins/_empty-state.scss +30 -0
  195. package/scss/config/mixins/_focus.scss +55 -0
  196. package/scss/config/mixins/_footer.scss +42 -0
  197. package/scss/config/mixins/_form.scss +601 -0
  198. package/scss/config/mixins/_index.scss +58 -0
  199. package/scss/config/mixins/_interaction.scss +15 -0
  200. package/scss/config/mixins/_kbd.scss +22 -0
  201. package/scss/config/mixins/_layout.scss +117 -0
  202. package/scss/config/mixins/_link.scss +55 -0
  203. package/scss/config/mixins/_ln-table.scss +420 -0
  204. package/scss/config/mixins/_loader.scss +26 -0
  205. package/scss/config/mixins/_modal.scss +66 -0
  206. package/scss/config/mixins/_motion.scss +19 -0
  207. package/scss/config/mixins/_nav.scss +273 -0
  208. package/scss/config/mixins/_page-header.scss +69 -0
  209. package/scss/config/mixins/_popover.scss +25 -0
  210. package/scss/config/mixins/_position.scss +32 -0
  211. package/scss/config/mixins/_progress.scss +56 -0
  212. package/scss/config/mixins/_prose.scss +127 -0
  213. package/scss/config/mixins/_shadows.scss +8 -0
  214. package/scss/config/mixins/_sidebar.scss +95 -0
  215. package/scss/config/mixins/_sizing.scss +6 -0
  216. package/scss/config/mixins/_spacing.scss +19 -0
  217. package/scss/config/mixins/_stat-card.scss +68 -0
  218. package/scss/config/mixins/_status-badge.scss +83 -0
  219. package/scss/config/mixins/_stepper.scss +78 -0
  220. package/scss/config/mixins/_table.scss +215 -0
  221. package/scss/config/mixins/_tabs.scss +64 -0
  222. package/scss/config/mixins/_timeline.scss +69 -0
  223. package/scss/config/mixins/_toast.scss +148 -0
  224. package/scss/config/mixins/_tooltip.scss +111 -0
  225. package/scss/config/mixins/_transitions.scss +10 -0
  226. package/scss/config/mixins/_translations.scss +124 -0
  227. package/scss/config/mixins/_typography.scss +57 -0
  228. package/scss/config/mixins/_upload.scss +168 -0
  229. package/scss/ln-ashlar.scss +62 -0
  230. package/scss/tabler-icons.txt +5039 -0
  231. package/scss/utilities/_animations.scss +83 -0
  232. package/scss/utilities/_utilities.scss +49 -0
@@ -0,0 +1,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
+ })();