@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,150 @@
1
+ # `data-ln-api-connector`
2
+
3
+ A zero-dependency, Local-First sync transport component that implements the Transport Gateway pattern of `ln-ashlar`.
4
+
5
+ This component encapsulates all connection parameters (base URLs, auth tokens, headers, paths) and provides a declarative, event-driven, or programmatic way to talk to any RESTful or JSON API backend. It isolates networking concerns completely, making your cache store (`data-ln-data-store`) and visual presentation layers fully network-agnostic.
6
+
7
+ ---
8
+
9
+ ## Declarative DOM Setup
10
+
11
+ Place the connector inside your parent coordinator element alongside your store:
12
+
13
+ ```html
14
+ <div data-ln-data-coordinator="documents">
15
+ <!-- Storage Layer Cache (Blind to networking) -->
16
+ <div data-ln-data-store
17
+ data-ln-store-indexes="status,updated_at">
18
+ </div>
19
+
20
+ <!-- Transport Gateway (REST / API Connector) -->
21
+ <div data-ln-api-connector
22
+ data-ln-api-base-url="https://api.livenetworks.com/v1"
23
+ data-ln-api-path="/documents"
24
+ data-ln-api-headers='{"Authorization": "Bearer tok_123"}'>
25
+ </div>
26
+ </div>
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Attributes
32
+
33
+ All attributes are dynamically observed. Any runtime changes to these attributes instantly update the connector's internal configurations.
34
+
35
+ | Attribute | Category | Description |
36
+ |-----------|----------|-------------|
37
+ | `data-ln-api-connector` | Selector | Creates the component instance. Optional value serves as name. |
38
+ | `data-ln-api-base-url` | Connection | The base URL of the API gateway (e.g. `https://api.example.com/v1` or `/api`). Fallback: `data-ln-api-connector-base-url`, `data-ln-rest-base-url`. |
39
+ | `data-ln-api-path` | Connection | Resource path endpoint (e.g. `/documents`). Fallback: `data-ln-api-connector-path`, `data-ln-rest-path`. |
40
+ | `data-ln-api-headers` | Credentials | JSON-formatted string of request headers (e.g. `{"Authorization": "Bearer tok_123"}`). Fallback: `data-ln-api-connector-headers`, `data-ln-rest-headers`. |
41
+
42
+ ---
43
+
44
+ ## JavaScript API Methods
45
+
46
+ You can access the instance methods directly on the element via the `.lnApiConnector` or `.lnConnector` properties:
47
+
48
+ ```javascript
49
+ const connectorEl = document.querySelector('[data-ln-api-connector]');
50
+ const connector = connectorEl.lnApiConnector; // or connectorEl.lnConnector
51
+
52
+ // 1. Fetch changed records since a Unix timestamp (delta protocol)
53
+ connector.fetchDelta(1736952600)
54
+ .then(data => console.log('Sync arrays loaded:', data));
55
+
56
+ // 2. Create a new record
57
+ connector.create({ title: 'New Document', status: 'Draft' })
58
+ .then(record => console.log('Created record:', record));
59
+
60
+ // 3. Update an existing record
61
+ connector.update(42, { title: 'Updated Title' })
62
+ .then(record => console.log('Updated record:', record));
63
+
64
+ // 4. Delete a record
65
+ connector.delete(42)
66
+ .then(res => console.log('Deleted:', res));
67
+
68
+ // 5. Bulk Delete records
69
+ connector.bulkDelete([17, 23])
70
+ .then(res => console.log('Bulk deleted:', res));
71
+ ```
72
+
73
+ ---
74
+
75
+ ## DOM Events
76
+
77
+ ### Commands (Dispatched TO the connector)
78
+
79
+ You can trigger mutations and fetches asynchronously by dispatching standard events directly on the connector DOM element. All events are supported in both `ln-api-connector` and legacy `ln-rest-connector` namespaces.
80
+
81
+ | Event | `detail` Payload | Description |
82
+ |-------|------------------|-------------|
83
+ | `ln-api-connector:request-sync` | `{ since }` | Triggers a delta fetch request. |
84
+ | `ln-api-connector:request-create` | `{ data, tempId }` | Triggers a creation request. |
85
+ | `ln-api-connector:request-update` | `{ id, data, expected_version }` | Triggers a PUT update (supports 409 conflict checks). |
86
+ | `ln-api-connector:request-delete` | `{ id }` | Triggers a deletion request. |
87
+ | `ln-api-connector:request-bulk-delete` | `{ ids }` | Triggers a bulk-deletion request. |
88
+
89
+ ### Notifications (Emitted BY the connector)
90
+
91
+ The connector dispatches bubbles-enabled events to notify parent coordinators of response states.
92
+
93
+ | Event | `detail` Payload | Description |
94
+ |-------|------------------|-------------|
95
+ | `ln-api-connector:fetched` | `{ data, since }` | Dispatched upon successful fetch completion. |
96
+ | `ln-api-connector:created` | `{ record, tempId }` | Dispatched upon successful server record confirmation. |
97
+ | `ln-api-connector:updated` | `{ record, id }` | Dispatched upon successful server update confirmation. |
98
+ | `ln-api-connector:deleted` | `{ response, id }` | Dispatched upon successful server deletion. |
99
+ | `ln-api-connector:bulk-deleted`| `{ response, ids }` | Dispatched upon successful server bulk deletion. |
100
+ | `ln-api-connector:error` | `{ action, error, status, ... }` | Dispatched on failures. Includes `conflictData` if status is 409. |
101
+ | `ln-api-connector:config-changed` | `{ baseUrl, path, headers }` | Dispatched when configuration attributes are mutated. |
102
+
103
+ ---
104
+
105
+ ## 3-Tier Integration Architecture (Coordinator Example)
106
+
107
+ Here is how a parent Coordinator links a Cache Store and the API Connector using standard DOM events:
108
+
109
+ ```javascript
110
+ (function () {
111
+ const parent = document.querySelector('[data-ln-data-coordinator="documents"]');
112
+ const storeEl = parent.querySelector('[data-ln-data-store]');
113
+ const connectorEl = parent.querySelector('[data-ln-api-connector]');
114
+
115
+ if (!storeEl || !connectorEl) return;
116
+
117
+ // 1. Storage needs remote sync -> forward request to Connector
118
+ storeEl.addEventListener('ln-store:request-remote-sync', function (e) {
119
+ connectorEl.dispatchEvent(new CustomEvent('ln-api-connector:request-sync', {
120
+ detail: { since: e.detail.since }
121
+ }));
122
+ });
123
+
124
+ // 2. Connector finishes sync -> feed delta back to Storage
125
+ connectorEl.addEventListener('ln-api-connector:fetched', function (e) {
126
+ const payload = e.detail.data;
127
+ storeEl.lnStore.applySync(payload.data, payload.deleted || [], payload.synced_at);
128
+ });
129
+
130
+ // 3. Storage requests remote creation -> forward payload to Connector
131
+ storeEl.addEventListener('ln-store:request-remote-create', function (e) {
132
+ connectorEl.dispatchEvent(new CustomEvent('ln-api-connector:request-create', {
133
+ detail: { data: e.detail.data, tempId: e.detail.tempId }
134
+ }));
135
+ });
136
+
137
+ // 4. Connector confirms creation -> swap optimistic temp records in Storage
138
+ connectorEl.addEventListener('ln-api-connector:created', function (e) {
139
+ storeEl.lnStore.confirmMutation(e.detail.tempId, e.detail.record, 'create');
140
+ });
141
+
142
+ // 5. In case of network errors -> trigger rollback in Cache Store
143
+ connectorEl.addEventListener('ln-api-connector:error', function (e) {
144
+ if (e.detail.action === 'create') {
145
+ storeEl.lnStore.revertMutation(e.detail.tempId, 'create', e.detail.error);
146
+ }
147
+ // ... handle updates and deletions similarly
148
+ });
149
+ })();
150
+ ```
@@ -0,0 +1 @@
1
+ (function(){"use strict";function c(i,o,d){i.dispatchEvent(new CustomEvent(o,{bubbles:!0,detail:d||{}}))}function E(i,o){if(!document.body){document.addEventListener("DOMContentLoaded",function(){E(i,o)}),console.warn("["+o+'] Script loaded before <body> — add "defer" to your <script> tag');return}i()}function y(i,o,d,u){if(i.nodeType!==1)return;const p=o.indexOf("[")!==-1||o.indexOf(".")!==-1||o.indexOf("#")!==-1?o:"["+o+"]",e=Array.from(i.querySelectorAll(p));i.matches&&i.matches(p)&&e.push(i);for(const n of e)n[d]||(n[d]=new u(n))}function T(i,o,d,u,h={}){const p=h.extraAttributes||[],e=h.onAttributeChange||null,n=h.onInit||null;function t(r){const a=r||document.body;y(a,i,o,d),n&&n(a)}return E(function(){const r=new MutationObserver(function(s){for(let f=0;f<s.length;f++){const l=s[f];if(l.type==="childList")for(let w=0;w<l.addedNodes.length;w++){const g=l.addedNodes[w];g.nodeType===1&&(y(g,i,o,d),n&&n(g))}else l.type==="attributes"&&(e&&l.target[o]?e(l.target,l.attributeName):(y(l.target,i,o,d),n&&n(l.target)))}});let a=[];if(i.indexOf("[")!==-1){const s=/\[([\w-]+)/g;let f;for(;(f=s.exec(i))!==null;)a.push(f[1])}else a.push(i);r.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:a.concat(p)})},u),window[o]=t,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){t(document.body)}):t(document.body),t}function m(...i){return i.filter(o=>o!=null&&o!=="").map((o,d)=>d===0?o.replace(/\/+$/,""):o.replace(/^\/+/,"").replace(/\/+$/,"")).filter(Boolean).join("/")}function b(i,o){return Object.assign({"Content-Type":"application/json",Accept:"application/json"},i,null)}function L(i,o="ln-core"){try{return i?JSON.parse(i):{}}catch(d){return console.error(`[${o}] Invalid headers JSON:`,d),{}}}const v={};function _(i,o){v[i]=o}function C(i){return v[i]||{ingress:o=>o,egress:o=>o}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=_,window.lnCore.getDataMapper=C),(function(){const i="data-ln-api-connector",o="lnApiConnector",d="lnConnector";if(window[o]!==void 0)return;function u(e){return this.dom=e,e[o]=this,e[d]=this,this.refreshConfig(),this._handlers=null,h(this),this}u.prototype.refreshConfig=function(){const e=this.dom;this.baseUrl=e.getAttribute("data-ln-api-base-url")||"",this.path=e.getAttribute("data-ln-api-path")||"",this.credentials="same-origin";const n=e.getAttribute("data-ln-api-headers")||"";this.headers=L(n,"ln-api-connector"),(n.toLowerCase().includes("authorization")||n.toLowerCase().includes("bearer")||n.toLowerCase().includes("basic"))&&console.warn("[ln-api-connector] Security Warning: Sensitive authorization credentials detected in data-ln-api-headers attribute. Storing secrets in HTML DOM attributes is highly discouraged and vulnerable to XSS credential extraction. Please use HttpOnly session cookies or a Backend Proxy Gateway instead."),c(e,"ln-api-connector:config-changed",{baseUrl:this.baseUrl,path:this.path,headers:this.headers})},u.prototype.fetchDelta=function(e){const n=this;let t=m(n.baseUrl,n.path);return e!=null&&e!==""&&(t+=(t.indexOf("?")!==-1?"&":"?")+"since="+encodeURIComponent(e)),window.fetch(t,{method:"GET",headers:b(n.headers),credentials:n.credentials}).then(r=>{if(!r.ok)throw new Error("HTTP "+r.status+": "+r.statusText);return r.json()})},u.prototype.create=function(e){const n=this;return window.fetch(m(n.baseUrl,n.path),{method:"POST",headers:b(n.headers),credentials:n.credentials,body:JSON.stringify(e)}).then(t=>{if(!t.ok)throw new Error("HTTP "+t.status+": "+t.statusText);return t.json()})},u.prototype.update=function(e,n){const t=this;return window.fetch(m(t.baseUrl,t.path,e),{method:"PUT",headers:b(t.headers),credentials:t.credentials,body:JSON.stringify(n)}).then(r=>{if(r.ok)return r.json();if(r.status===409)return r.json().then(a=>{const s=new Error("Conflict");throw s.status=409,s.data=a,s});throw new Error("HTTP "+r.status+": "+r.statusText)})},u.prototype.delete=function(e){const n=this;return window.fetch(m(n.baseUrl,n.path,e),{method:"DELETE",headers:b(n.headers),credentials:n.credentials}).then(t=>{if(!t.ok)throw new Error("HTTP "+t.status+": "+t.statusText);return t.json()})},u.prototype.bulkDelete=function(e){const n=this;return window.fetch(m(n.baseUrl,n.path)+"/bulk-delete",{method:"DELETE",headers:b(n.headers),credentials:n.credentials,body:JSON.stringify({ids:e})}).then(t=>{if(!t.ok)throw new Error("HTTP "+t.status+": "+t.statusText);return t.json()})};function h(e){e._handlers={sync:function(t){const r=t.detail||{};e.fetchDelta(r.since).then(function(a){c(e.dom,"ln-api-connector:fetched",{data:a,since:r.since})}).catch(function(a){c(e.dom,"ln-api-connector:error",{action:"sync",error:a.message,status:a.status||0,since:r.since})})},create:function(t){const r=t.detail||{};e.create(r.data).then(function(a){c(e.dom,"ln-api-connector:created",{record:a,tempId:r.tempId})}).catch(function(a){c(e.dom,"ln-api-connector:error",{action:"create",error:a.message,status:a.status||0,tempId:r.tempId})})},update:function(t){const r=t.detail||{},a=Object.assign({},r.data);r.expected_version!==void 0&&(a.expected_version=r.expected_version),e.update(r.id,a).then(function(s){c(e.dom,"ln-api-connector:updated",{record:s,id:r.id})}).catch(function(s){c(e.dom,"ln-api-connector:error",{action:"update",error:s.message,status:s.status||0,id:r.id,conflictData:s.status===409?s.data:null})})},delete:function(t){const r=t.detail||{};e.delete(r.id).then(function(a){c(e.dom,"ln-api-connector:deleted",{response:a,id:r.id})}).catch(function(a){c(e.dom,"ln-api-connector:error",{action:"delete",error:a.message,status:a.status||0,id:r.id})})},bulkDelete:function(t){const r=t.detail||{};e.bulkDelete(r.ids).then(function(a){c(e.dom,"ln-api-connector:bulk-deleted",{response:a,ids:r.ids})}).catch(function(a){c(e.dom,"ln-api-connector:error",{action:"bulk-delete",error:a.message,status:a.status||0,ids:r.ids})})}},["ln-api-connector","ln-rest-connector"].forEach(function(t){e.dom.addEventListener(t+":request-sync",e._handlers.sync),e.dom.addEventListener(t+":request-fetch",e._handlers.sync),e.dom.addEventListener(t+":request-create",e._handlers.create),e.dom.addEventListener(t+":request-update",e._handlers.update),e.dom.addEventListener(t+":request-delete",e._handlers.delete),e.dom.addEventListener(t+":request-bulk-delete",e._handlers.bulkDelete)})}u.prototype.destroy=function(){if(!this.dom[o])return;const e=this;e._handlers&&(["ln-api-connector","ln-rest-connector"].forEach(function(t){e.dom.removeEventListener(t+":request-sync",e._handlers.sync),e.dom.removeEventListener(t+":request-fetch",e._handlers.sync),e.dom.removeEventListener(t+":request-create",e._handlers.create),e.dom.removeEventListener(t+":request-update",e._handlers.update),e.dom.removeEventListener(t+":request-delete",e._handlers.delete),e.dom.removeEventListener(t+":request-bulk-delete",e._handlers.bulkDelete)}),e._handlers=null),c(this.dom,"ln-api-connector:destroyed",{target:this.dom}),delete this.dom[o],delete this.dom[d]};function p(e){const n=e[o];n&&n.refreshConfig()}T(i,o,u,"ln-api-connector",{extraAttributes:["data-ln-api-base-url","data-ln-api-path","data-ln-api-headers"],onAttributeChange:p})})()})();
@@ -0,0 +1,265 @@
1
+ import { registerComponent, dispatch, buildUrl, getHeaders, parseHeaders } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-api-connector';
5
+ const DOM_ATTRIBUTE = 'lnApiConnector';
6
+ const DOM_ALIAS = 'lnConnector';
7
+
8
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
9
+
10
+ // ─── Component Constructor ─────────────────────────────
11
+
12
+ function _component(dom) {
13
+ this.dom = dom;
14
+ dom[DOM_ATTRIBUTE] = this;
15
+ dom[DOM_ALIAS] = this; // Set alias for compatibility
16
+
17
+ this.refreshConfig();
18
+ this._handlers = null;
19
+ _bindEvents(this);
20
+
21
+ return this;
22
+ }
23
+
24
+ // ─── Refresh Config from DOM Attributes ─────────────────
25
+
26
+ _component.prototype.refreshConfig = function () {
27
+ const dom = this.dom;
28
+
29
+ this.baseUrl = dom.getAttribute('data-ln-api-base-url') || '';
30
+ this.path = dom.getAttribute('data-ln-api-path') || '';
31
+ this.credentials = 'same-origin';
32
+
33
+ const rawHeaders = dom.getAttribute('data-ln-api-headers') || '';
34
+ this.headers = parseHeaders(rawHeaders, 'ln-api-connector');
35
+
36
+ if (rawHeaders.toLowerCase().includes('authorization') || rawHeaders.toLowerCase().includes('bearer') || rawHeaders.toLowerCase().includes('basic')) {
37
+ console.warn('[ln-api-connector] Security Warning: Sensitive authorization credentials detected in data-ln-api-headers attribute. Storing secrets in HTML DOM attributes is highly discouraged and vulnerable to XSS credential extraction. Please use HttpOnly session cookies or a Backend Proxy Gateway instead.');
38
+ }
39
+
40
+ dispatch(dom, 'ln-api-connector:config-changed', {
41
+ baseUrl: this.baseUrl,
42
+ path: this.path,
43
+ headers: this.headers
44
+ });
45
+ };
46
+
47
+ // ─── JS API Methods (Promises) ──────────────────────────
48
+
49
+ _component.prototype.fetchDelta = function (since) {
50
+ const self = this;
51
+ let url = buildUrl(self.baseUrl, self.path);
52
+
53
+ if (since !== undefined && since !== null && since !== '') {
54
+ url += (url.indexOf('?') !== -1 ? '&' : '?') + 'since=' + encodeURIComponent(since);
55
+ }
56
+
57
+ return window.fetch(url, { method: 'GET', headers: getHeaders(self.headers), credentials: self.credentials })
58
+ .then(res => {
59
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
60
+ return res.json();
61
+ });
62
+ };
63
+
64
+ _component.prototype.create = function (payload) {
65
+ const self = this;
66
+ return window.fetch(buildUrl(self.baseUrl, self.path), {
67
+ method: 'POST',
68
+ headers: getHeaders(self.headers),
69
+ credentials: self.credentials,
70
+ body: JSON.stringify(payload)
71
+ })
72
+ .then(res => {
73
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
74
+ return res.json();
75
+ });
76
+ };
77
+
78
+ _component.prototype.update = function (id, payload) {
79
+ const self = this;
80
+ return window.fetch(buildUrl(self.baseUrl, self.path, id), {
81
+ method: 'PUT',
82
+ headers: getHeaders(self.headers),
83
+ credentials: self.credentials,
84
+ body: JSON.stringify(payload)
85
+ })
86
+ .then(res => {
87
+ if (res.ok) return res.json();
88
+ if (res.status === 409) return res.json().then(data => {
89
+ const err = new Error('Conflict');
90
+ err.status = 409;
91
+ err.data = data;
92
+ throw err;
93
+ });
94
+ throw new Error('HTTP ' + res.status + ': ' + res.statusText);
95
+ });
96
+ };
97
+
98
+ _component.prototype.delete = function (id) {
99
+ const self = this;
100
+ return window.fetch(buildUrl(self.baseUrl, self.path, id), {
101
+ method: 'DELETE',
102
+ headers: getHeaders(self.headers),
103
+ credentials: self.credentials
104
+ })
105
+ .then(res => {
106
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
107
+ return res.json();
108
+ });
109
+ };
110
+
111
+ _component.prototype.bulkDelete = function (ids) {
112
+ const self = this;
113
+ return window.fetch(buildUrl(self.baseUrl, self.path) + '/bulk-delete', {
114
+ method: 'DELETE',
115
+ headers: getHeaders(self.headers),
116
+ credentials: self.credentials,
117
+ body: JSON.stringify({ ids: ids })
118
+ })
119
+ .then(res => {
120
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
121
+ return res.json();
122
+ });
123
+ };
124
+
125
+ // ─── DOM Event Routing ────────────────────────────────────
126
+
127
+ function _bindEvents(self) {
128
+ self._handlers = {
129
+ sync: function (e) {
130
+ const detail = e.detail || {};
131
+ self.fetchDelta(detail.since)
132
+ .then(function (data) {
133
+ dispatch(self.dom, 'ln-api-connector:fetched', { data: data, since: detail.since });
134
+ })
135
+ .catch(function (err) {
136
+ dispatch(self.dom, 'ln-api-connector:error', {
137
+ action: 'sync',
138
+ error: err.message,
139
+ status: err.status || 0,
140
+ since: detail.since
141
+ });
142
+ });
143
+ },
144
+ create: function (e) {
145
+ const detail = e.detail || {};
146
+ self.create(detail.data)
147
+ .then(function (record) {
148
+ dispatch(self.dom, 'ln-api-connector:created', { record: record, tempId: detail.tempId });
149
+ })
150
+ .catch(function (err) {
151
+ dispatch(self.dom, 'ln-api-connector:error', {
152
+ action: 'create',
153
+ error: err.message,
154
+ status: err.status || 0,
155
+ tempId: detail.tempId
156
+ });
157
+ });
158
+ },
159
+ update: function (e) {
160
+ const detail = e.detail || {};
161
+ // Merge expected_version into payload for conflict checking
162
+ const payload = Object.assign({}, detail.data);
163
+ if (detail.expected_version !== undefined) {
164
+ payload.expected_version = detail.expected_version;
165
+ }
166
+ self.update(detail.id, payload)
167
+ .then(function (record) {
168
+ dispatch(self.dom, 'ln-api-connector:updated', { record: record, id: detail.id });
169
+ })
170
+ .catch(function (err) {
171
+ dispatch(self.dom, 'ln-api-connector:error', {
172
+ action: 'update',
173
+ error: err.message,
174
+ status: err.status || 0,
175
+ id: detail.id,
176
+ conflictData: err.status === 409 ? err.data : null
177
+ });
178
+ });
179
+ },
180
+ delete: function (e) {
181
+ const detail = e.detail || {};
182
+ self.delete(detail.id)
183
+ .then(function (res) {
184
+ dispatch(self.dom, 'ln-api-connector:deleted', { response: res, id: detail.id });
185
+ })
186
+ .catch(function (err) {
187
+ dispatch(self.dom, 'ln-api-connector:error', {
188
+ action: 'delete',
189
+ error: err.message,
190
+ status: err.status || 0,
191
+ id: detail.id
192
+ });
193
+ });
194
+ },
195
+ bulkDelete: function (e) {
196
+ const detail = e.detail || {};
197
+ self.bulkDelete(detail.ids)
198
+ .then(function (res) {
199
+ dispatch(self.dom, 'ln-api-connector:bulk-deleted', { response: res, ids: detail.ids });
200
+ })
201
+ .catch(function (err) {
202
+ dispatch(self.dom, 'ln-api-connector:error', {
203
+ action: 'bulk-delete',
204
+ error: err.message,
205
+ status: err.status || 0,
206
+ ids: detail.ids
207
+ });
208
+ });
209
+ }
210
+ };
211
+
212
+ // Support both general and REST namespaces for incoming requests
213
+ const namespaces = ['ln-api-connector', 'ln-rest-connector'];
214
+ namespaces.forEach(function (ns) {
215
+ self.dom.addEventListener(ns + ':request-sync', self._handlers.sync);
216
+ self.dom.addEventListener(ns + ':request-fetch', self._handlers.sync);
217
+ self.dom.addEventListener(ns + ':request-create', self._handlers.create);
218
+ self.dom.addEventListener(ns + ':request-update', self._handlers.update);
219
+ self.dom.addEventListener(ns + ':request-delete', self._handlers.delete);
220
+ self.dom.addEventListener(ns + ':request-bulk-delete', self._handlers.bulkDelete);
221
+ });
222
+ }
223
+
224
+ _component.prototype.destroy = function () {
225
+ if (!this.dom[DOM_ATTRIBUTE]) return;
226
+
227
+ const self = this;
228
+ if (self._handlers) {
229
+ const namespaces = ['ln-api-connector', 'ln-rest-connector'];
230
+ namespaces.forEach(function (ns) {
231
+ self.dom.removeEventListener(ns + ':request-sync', self._handlers.sync);
232
+ self.dom.removeEventListener(ns + ':request-fetch', self._handlers.sync);
233
+ self.dom.removeEventListener(ns + ':request-create', self._handlers.create);
234
+ self.dom.removeEventListener(ns + ':request-update', self._handlers.update);
235
+ self.dom.removeEventListener(ns + ':request-delete', self._handlers.delete);
236
+ self.dom.removeEventListener(ns + ':request-bulk-delete', self._handlers.bulkDelete);
237
+ });
238
+ self._handlers = null;
239
+ }
240
+
241
+ dispatch(this.dom, 'ln-api-connector:destroyed', { target: this.dom });
242
+
243
+ delete this.dom[DOM_ATTRIBUTE];
244
+ delete this.dom[DOM_ALIAS];
245
+ };
246
+
247
+ // ─── Attribute Sync ────────────────────────────────────────
248
+
249
+ function _syncAttribute(el) {
250
+ const instance = el[DOM_ATTRIBUTE];
251
+ if (!instance) return;
252
+ instance.refreshConfig();
253
+ }
254
+
255
+ // ─── Registration ──────────────────────────────────────
256
+
257
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-api-connector', {
258
+ extraAttributes: [
259
+ 'data-ln-api-base-url',
260
+ 'data-ln-api-path',
261
+ 'data-ln-api-headers'
262
+ ],
263
+ onAttributeChange: _syncAttribute
264
+ });
265
+ })();
@@ -0,0 +1,80 @@
1
+ # ln-autoresize
2
+
3
+ A zero-dependency, high-performance **UX Helper Primitive** (~47 lines of JavaScript) that dynamically resizes a `<textarea>` to track its content. It grows as the user types and collapses instantly as text is deleted.
4
+
5
+ ---
6
+
7
+ ## 🧭 Philosophy & Architecture
8
+
9
+ 1. **Platform-First Execution:** Instead of introducing heavy observers or layouts, `ln-autoresize` hooks directly into the browser's native layout engine. By resetting height to `auto` before reading `scrollHeight` inside the event loop, it forces a synchronous, flicker-free layout calculation.
10
+ 2. **Strict Concern Scope:** The primitive is stateless. Ceiling limits, minimum bounds, and manual resize handles are defined exclusively via standard CSS classes. The script only handles active observation.
11
+ 3. **Reactive Synchronization:** Data flows strictly through DOM events. Programmatic changes must trigger standard events (`input` / `change`) so that dependent primitives can adapt in synchrony.
12
+
13
+ ---
14
+
15
+ ## 📦 Minimal Blueprint
16
+
17
+ ```html
18
+ <textarea data-ln-autoresize rows="1" placeholder="Type here..."></textarea>
19
+ ```
20
+
21
+ To cap growth at a maximum height and enable scroll past it, pair the attribute with pure CSS:
22
+
23
+ ```scss
24
+ textarea[data-ln-autoresize] {
25
+ resize: none; // Removes conflicting manual drag handle
26
+ max-height: 6rem; // Defines height ceiling
27
+ overflow-y: auto; // Reveals scrollbar at ceiling
28
+ }
29
+ ```
30
+
31
+ > [!TIP]
32
+ > Always set `rows="1"` on the HTML element. This ensures the initial empty height matches the post-initialization state and prevents visual snap-shut on first paint.
33
+
34
+ ---
35
+
36
+ ## 🛠️ Declarative API Contract
37
+
38
+ ### HTML Attributes
39
+
40
+ | Attribute | Elements | Description |
41
+ | :--- | :--- | :--- |
42
+ | `data-ln-autoresize` | `<textarea>` | Observation marker. Presence creates the instance and runs initial measurement. |
43
+
44
+ ### JS API
45
+
46
+ Access the utility instance directly via the `lnAutoresize` property on the textarea element:
47
+
48
+ ```javascript
49
+ const textarea = document.getElementById('my-textarea');
50
+
51
+ // 1. Force a manual re-measure (essential after parent reveals, font loads, etc.)
52
+ textarea.lnAutoresize._resize();
53
+
54
+ // 2. Tear down event listeners and clear inline styles
55
+ textarea.lnAutoresize.destroy();
56
+ ```
57
+
58
+ ---
59
+
60
+ ## ⚡ DOM Events
61
+
62
+ `ln-autoresize` does not emit custom events. It relies entirely on standard browser interactions:
63
+
64
+ - **Listens to `input`:** Triggers `_resize()` on keystrokes, pastes, and deletes.
65
+ - **Listens to `change`:** Triggers `_resize()` on value commits.
66
+
67
+ ---
68
+
69
+ ## ⚠️ Common Pitfalls
70
+
71
+ - **Setting `textarea.value` directly:** Programmatic writes are silent in the DOM. The component will not detect value updates unless you dispatch a synthetic event:
72
+ ```javascript
73
+ textarea.value = 'New text content';
74
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
75
+ ```
76
+ - **Mounting in hidden parents:** A textarea inside a `display: none` container measures `scrollHeight` as `0`. When revealed later, it will appear collapsed. Force a re-measure manually after reveal:
77
+ ```javascript
78
+ textarea.lnAutoresize._resize();
79
+ ```
80
+ - **Conflicting manual drag handles:** By default, textareas may have `resize: vertical` applied. This lets users manual resize the input, which is instantly overridden by the script on the next keystroke. Set `resize: none` to clean up the interface.
@@ -0,0 +1 @@
1
+ (function(){"use strict";function y(e,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){y(e,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}e()}function h(e,t,n,r){if(e.nodeType!==1)return;const f=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",a=Array.from(e.querySelectorAll(f));e.matches&&e.matches(f)&&a.push(e);for(const i of a)i[n]||(i[n]=new r(i))}function b(e,t,n,r,s={}){const f=s.extraAttributes||[],a=s.onAttributeChange||null,i=s.onInit||null;function c(p){const d=p||document.body;h(d,e,t,n),i&&i(d)}return y(function(){const p=new MutationObserver(function(l){for(let u=0;u<l.length;u++){const o=l[u];if(o.type==="childList")for(let g=0;g<o.addedNodes.length;g++){const m=o.addedNodes[g];m.nodeType===1&&(h(m,e,t,n),i&&i(m))}else o.type==="attributes"&&(a&&o.target[t]?a(o.target,o.attributeName):(h(o.target,e,t,n),i&&i(o.target)))}});let d=[];if(e.indexOf("[")!==-1){const l=/\[([\w-]+)/g;let u;for(;(u=l.exec(e))!==null;)d.push(u[1])}else d.push(e);p.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:d.concat(f)})},r),window[t]=c,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){c(document.body)}):c(document.body),c}const w={};function x(e,t){w[e]=t}function A(e){return w[e]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=x,window.lnCore.getDataMapper=A),(function(){const e="data-ln-autoresize",t="lnAutoresize";if(window[t]!==void 0)return;function n(r){if(r.tagName!=="TEXTAREA")return console.warn("[ln-autoresize] Can only be applied to <textarea>, got:",r.tagName),this;this.dom=r;const s=this;return this._onInput=function(){s._resize()},r.addEventListener("input",this._onInput),this._resize(),this}n.prototype._resize=function(){this.dom.style.height="auto",this.dom.style.height=this.dom.scrollHeight+"px"},n.prototype.destroy=function(){this.dom[t]&&(this.dom.removeEventListener("input",this._onInput),this.dom.style.height="",delete this.dom[t])},b(e,t,n,"ln-autoresize")})()})();
@@ -0,0 +1,47 @@
1
+ import { registerComponent } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-autoresize';
5
+ const DOM_ATTRIBUTE = 'lnAutoresize';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ // ─── Component ─────────────────────────────────────────────
10
+
11
+ function _component(dom) {
12
+ if (dom.tagName !== 'TEXTAREA') {
13
+ console.warn('[ln-autoresize] Can only be applied to <textarea>, got:', dom.tagName);
14
+ return this;
15
+ }
16
+
17
+ this.dom = dom;
18
+
19
+ const self = this;
20
+ this._onInput = function () {
21
+ self._resize();
22
+ };
23
+
24
+ dom.addEventListener('input', this._onInput);
25
+
26
+ // Initial resize (content may be pre-filled)
27
+ this._resize();
28
+
29
+ return this;
30
+ }
31
+
32
+ _component.prototype._resize = function () {
33
+ this.dom.style.height = 'auto';
34
+ this.dom.style.height = this.dom.scrollHeight + 'px';
35
+ };
36
+
37
+ _component.prototype.destroy = function () {
38
+ if (!this.dom[DOM_ATTRIBUTE]) return;
39
+ this.dom.removeEventListener('input', this._onInput);
40
+ this.dom.style.height = '';
41
+ delete this.dom[DOM_ATTRIBUTE];
42
+ };
43
+
44
+ // ─── Init ──────────────────────────────────────────────────
45
+
46
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-autoresize');
47
+ })();
@@ -0,0 +1,92 @@
1
+ # ln-autosave
2
+
3
+ A zero-dependency, localStorage-backed **Draft Buffer Primitive** that prevents data loss by capturing form states. It automatically saves drafts on field boundaries, restores values on load, and clears them cleanly on submit, reset, or custom cancellations.
4
+
5
+ ---
6
+
7
+ ## 🧭 Philosophy & Architecture
8
+
9
+ 1. **Synchronous local storage:** Unlike async network databases, `ln-autosave` uses browser `localStorage`. This ensures drafts are written instantly on field blur/leave (`focusout` / `change`), eliminating race conditions during page changes.
10
+ 2. **Zero-Keystroke flood:** By default, writing only triggers on field blur. For continuous typing fields (like editors), an opt-in debounced input listener can be configured.
11
+ 3. **Decoupled Event Restoration:** restored values are applied by dispatching standard synthetic `input` and `change` events. This ensures sibling primitives (such as `ln-validate` and `ln-autoresize`) re-evaluate automatically.
12
+
13
+ ---
14
+
15
+ ## 📦 Minimal Blueprint
16
+
17
+ ```html
18
+ <form id="profile-edit" data-ln-autosave>
19
+ <div class="form-element">
20
+ <label for="bio">Biography</label>
21
+ <textarea id="bio" name="bio" data-ln-autoresize rows="1"></textarea>
22
+ </div>
23
+
24
+ <ul class="form-actions">
25
+ <li><button type="button" data-ln-autosave-clear>Cancel (clear draft)</button></li>
26
+ <li><button type="submit">Save</button></li>
27
+ </ul>
28
+ </form>
29
+ ```
30
+
31
+ > [!IMPORTANT]
32
+ > The form **must** have a unique `id` or you must specify a custom identifier in `data-ln-autosave="identifier"`. The final storage key is scoped uniquely as `ln-autosave:{pathname}:{identifier}`.
33
+
34
+ ---
35
+
36
+ ## 🛠️ Declarative API Contract
37
+
38
+ ### HTML Attributes
39
+
40
+ | Attribute | Elements | Description |
41
+ | :--- | :--- | :--- |
42
+ | `data-ln-autosave` | `<form>` | Persistence marker. Value overrides the form ID lookup. |
43
+ | `data-ln-autosave-clear` | `<button>` | Click delegate. Instantly purges the localStorage entry. |
44
+ | `data-ln-autosave-debounce-input="ms"` | `<form>` | Opt-in. Saves on idle keystrokes (defaults to 1000ms if empty). |
45
+
46
+ ### JS API
47
+
48
+ Access the persistence instance directly via the `lnAutoresize` property on the form element:
49
+
50
+ ```javascript
51
+ const form = document.getElementById('profile-edit');
52
+
53
+ // 1. Back-reference properties
54
+ const storageKey = form.lnAutosave.key; // "ln-autosave:/path:profile-edit"
55
+
56
+ // 2. Clean up listeners and pending timers
57
+ form.lnAutosave.destroy();
58
+ ```
59
+
60
+ ---
61
+
62
+ ## ⚡ DOM Events
63
+
64
+ ### Emitted
65
+
66
+ | Event | Bubbles | Payload | Description |
67
+ | :--- | :--- | :--- | :--- |
68
+ | `ln-autosave:before-restore` | Yes (Cancelable) | `{ target, data }` | Fires before applying draft. Call `e.preventDefault()` to abort. |
69
+ | `ln-autosave:restored` | Yes | `{ target, data }` | Dispatched after populated values and synthetic events are sent. |
70
+ | `ln-autosave:saved` | Yes | `{ target, data }` | Dispatched on successful localStorage save. |
71
+ | `ln-autosave:cleared` | Yes | `{ target }` | Dispatched after localStorage item removal. |
72
+
73
+ ---
74
+
75
+ ## ⚠️ Common Pitfalls
76
+
77
+ - **Stale Drafts Overwriting Server Data:** On pages where the database has rendered new state, draft restores must be aborted:
78
+ ```javascript
79
+ document.addEventListener('ln-autosave:before-restore', function (e) {
80
+ if (e.target.dataset.hasServerData === 'true') {
81
+ e.preventDefault();
82
+ localStorage.removeItem(e.target.lnAutosave.key);
83
+ }
84
+ });
85
+ ```
86
+ - **ESC Modal Dismissals:** If a user closes a modal using the ESC key, no click triggers on cancel buttons. Wire draft cleanup to the modal's closed event manually:
87
+ ```javascript
88
+ modal.addEventListener('ln-modal:closed', () => {
89
+ localStorage.removeItem(form.lnAutosave.key);
90
+ });
91
+ ```
92
+ - **Nameless & File Inputs:** `ln-autosave` ignores disabled inputs, button elements, `type="file"`, and inputs missing a `name` attribute.
@@ -0,0 +1 @@
1
+ (function(){"use strict";function E(o,e,u){o.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:u||{}}))}function A(o,e,u){const s=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:u||{}});return o.dispatchEvent(s),s}function C(o,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){C(o,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}o()}function y(o,e,u,s){if(o.nodeType!==1)return;const i=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",l=Array.from(o.querySelectorAll(i));o.matches&&o.matches(i)&&l.push(o);for(const c of l)c[u]||(c[u]=new s(c))}function O(o){const e={},u=o.elements;for(let s=0;s<u.length;s++){const n=u[s];if(!(!n.name||n.disabled||n.type==="file"||n.type==="submit"||n.type==="button"))if(n.type==="checkbox")e[n.name]||(e[n.name]=[]),n.checked&&e[n.name].push(n.value);else if(n.type==="radio")n.checked&&(e[n.name]=n.value);else if(n.type==="select-multiple"){e[n.name]=[];for(let i=0;i<n.options.length;i++)n.options[i].selected&&e[n.name].push(n.options[i].value)}else e[n.name]=n.value}return e}function S(o,e){const u=o.elements,s=[];for(let n=0;n<u.length;n++){const i=u[n];if(!i.name||!(i.name in e)||i.type==="file"||i.type==="submit"||i.type==="button")continue;const l=e[i.name];if(i.type==="checkbox")i.checked=Array.isArray(l)?l.indexOf(i.value)!==-1:!!l,s.push(i);else if(i.type==="radio")i.checked=i.value===String(l),s.push(i);else if(i.type==="select-multiple"){if(Array.isArray(l))for(let c=0;c<i.options.length;c++)i.options[c].selected=l.indexOf(i.options[c].value)!==-1;s.push(i)}else i.value=l,s.push(i)}return s}function T(o,e,u,s,n={}){const i=n.extraAttributes||[],l=n.onAttributeChange||null,c=n.onInit||null;function p(b){const t=b||document.body;y(t,o,e,u),c&&c(t)}return C(function(){const b=new MutationObserver(function(a){for(let r=0;r<a.length;r++){const h=a[r];if(h.type==="childList")for(let g=0;g<h.addedNodes.length;g++){const v=h.addedNodes[g];v.nodeType===1&&(y(v,o,e,u),c&&c(v))}else h.type==="attributes"&&(l&&h.target[e]?l(h.target,h.attributeName):(y(h.target,o,e,u),c&&c(h.target)))}});let t=[];if(o.indexOf("[")!==-1){const a=/\[([\w-]+)/g;let r;for(;(r=a.exec(o))!==null;)t.push(r[1])}else t.push(o);b.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:t.concat(i)})},s),window[e]=p,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){p(document.body)}):p(document.body),p}const w={};function D(o,e){w[o]=e}function I(o){return w[o]||{ingress:e=>e,egress:e=>e}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=D,window.lnCore.getDataMapper=I),(function(){const o="data-ln-autosave",e="lnAutosave",u="data-ln-autosave-clear",s="data-ln-autosave-debounce-input",n="ln-autosave:";if(window[e]!==void 0)return;function l(t){const a=c(t);if(!a){console.warn("ln-autosave: form needs an id or data-ln-autosave value",t);return}this.dom=t,this.key=a;let r=null;function h(){const d=O(t);try{localStorage.setItem(a,JSON.stringify(d))}catch{return}E(t,"ln-autosave:saved",{target:t,data:d})}function g(){let d;try{d=localStorage.getItem(a)}catch{return}if(!d)return;let f;try{f=JSON.parse(d)}catch{return}if(A(t,"ln-autosave:before-restore",{target:t,data:f}).defaultPrevented)return;const _=S(t,f);for(let m=0;m<_.length;m++)_[m].dispatchEvent(new Event("input",{bubbles:!0})),_[m].dispatchEvent(new Event("change",{bubbles:!0}));E(t,"ln-autosave:restored",{target:t,data:f})}function v(){try{localStorage.removeItem(a)}catch{return}E(t,"ln-autosave:cleared",{target:t})}this._onFocusout=function(d){const f=d.target;p(f)&&f.name&&h()},this._onChange=function(d){const f=d.target;p(f)&&f.name&&h()},this._onSubmit=function(){v()},this._onReset=function(){v()},this._onClearClick=function(d){d.target.closest("["+u+"]")&&v()},t.addEventListener("focusout",this._onFocusout),t.addEventListener("change",this._onChange),t.addEventListener("submit",this._onSubmit),t.addEventListener("reset",this._onReset),t.addEventListener("click",this._onClearClick);const L=b(t);return L>0&&(this._onInput=function(d){const f=d.target;!p(f)||!f.name||(r!==null&&clearTimeout(r),r=setTimeout(h,L))},t.addEventListener("input",this._onInput)),this._getInputTimer=function(){return r},g(),this}l.prototype.destroy=function(){if(this.dom[e]){if(this.dom.removeEventListener("focusout",this._onFocusout),this.dom.removeEventListener("change",this._onChange),this.dom.removeEventListener("submit",this._onSubmit),this.dom.removeEventListener("reset",this._onReset),this.dom.removeEventListener("click",this._onClearClick),this._onInput){this.dom.removeEventListener("input",this._onInput);const t=this._getInputTimer();t!==null&&clearTimeout(t)}E(this.dom,"ln-autosave:destroyed",{target:this.dom}),delete this.dom[e]}};function c(t){const r=t.getAttribute(o)||t.id;return r?n+window.location.pathname+":"+r:null}function p(t){const a=t.tagName;return a==="INPUT"||a==="TEXTAREA"||a==="SELECT"}function b(t){if(!t.hasAttribute(s))return 0;const a=t.getAttribute(s);if(a===""||a===null)return 1e3;const r=parseInt(a,10);return isNaN(r)||r<0?(console.warn("ln-autosave: invalid debounce value, using default",t),1e3):r}T(o,e,l,"ln-autosave")})()})();