@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,249 @@
1
+ import { registerComponent, dispatch } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-data-coordinator';
5
+ const DOM_ATTRIBUTE = 'lnDataCoordinator';
6
+ const DOM_ALIAS = 'lnCoordinator';
7
+
8
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
9
+
10
+ // ─── Component Constructor ─────────────────────────────
11
+
12
+ function _component(dom) {
13
+ this.dom = dom;
14
+ this._name = dom.getAttribute(DOM_SELECTOR);
15
+ dom[DOM_ATTRIBUTE] = this;
16
+ dom[DOM_ALIAS] = this;
17
+
18
+ this.mapper = null;
19
+ this._handlers = null;
20
+
21
+ this.refreshMapper();
22
+ _bindEvents(this);
23
+
24
+ return this;
25
+ }
26
+
27
+ // ─── Resolve and Refresh Mapper ──────────────────────────
28
+
29
+ _component.prototype.refreshMapper = function () {
30
+ this.mapper = null;
31
+
32
+ // 1. Check for deprecated/insecure inline script mapper
33
+ const inlineScript = this.dom.querySelector('script[data-ln-mapper]');
34
+ if (inlineScript) {
35
+ console.error('[ln-data-coordinator] Security Error: Inline script mappers using <script data-ln-mapper> are deprecated and disabled due to XSS vulnerability risks (unsafe-eval). Please register your mappers securely via window.lnCore.registerDataMapper() instead.');
36
+ }
37
+
38
+ // 2. Resolve to registered external mapper
39
+ const mapperName = this.dom.getAttribute('data-ln-data-mapper') || this.dom.getAttribute('data-ln-data-coordinator');
40
+ if (mapperName && window.lnCore && typeof window.lnCore.getDataMapper === 'function') {
41
+ this.mapper = window.lnCore.getDataMapper(mapperName);
42
+ }
43
+
44
+ // 3. Ultimate safe fallback: no-op mapper
45
+ if (!this.mapper) {
46
+ this.mapper = {};
47
+ }
48
+
49
+ // Ensure ingress and egress are safe callable functions
50
+ if (typeof this.mapper.ingress !== 'function') {
51
+ this.mapper.ingress = function (r) { return r; };
52
+ }
53
+ if (typeof this.mapper.egress !== 'function') {
54
+ this.mapper.egress = function (r) { return r; };
55
+ }
56
+ };
57
+
58
+ // ─── Dynamic Child Discovery ──────────────────────────────
59
+
60
+ _component.prototype.findChildren = function () {
61
+ const storeEl = this.dom.querySelector('[data-ln-data-store]');
62
+ const connectorEl = this.dom.querySelector('[data-ln-api-connector], [data-ln-couchdb-connector], [data-ln-websocket-connector], [data-ln-rest-connector]');
63
+
64
+ return {
65
+ storeEl: storeEl,
66
+ connectorEl: connectorEl,
67
+ store: storeEl ? (storeEl.lnDataStore || storeEl.lnStore) : null,
68
+ connector: connectorEl ? (connectorEl.lnConnector || connectorEl.lnApiConnector || connectorEl.lnCouchDbConnector) : null
69
+ };
70
+ };
71
+
72
+ // ─── Event Binding ────────────────────────────────────────
73
+
74
+ function _bindEvents(self) {
75
+ self._handlers = {
76
+ sync: function (e) {
77
+ self.refreshMapper();
78
+ const children = self.findChildren();
79
+ if (!children.store || !children.connector) {
80
+ console.warn('[ln-data-coordinator] Cannot sync: store or connector not found in subtree');
81
+ return;
82
+ }
83
+
84
+ const since = e.detail.since;
85
+
86
+ children.connector.fetchDelta(since)
87
+ .then(function (rawResponse) {
88
+ let rawRecords = [];
89
+ let deletedIds = [];
90
+ let syncedAt = null;
91
+
92
+ if (rawResponse && Array.isArray(rawResponse)) {
93
+ rawRecords = rawResponse;
94
+ syncedAt = Math.floor(Date.now() / 1000);
95
+ } else if (rawResponse) {
96
+ rawRecords = Array.isArray(rawResponse.data) ? rawResponse.data : [];
97
+ deletedIds = Array.isArray(rawResponse.deleted) ? rawResponse.deleted : [];
98
+ syncedAt = rawResponse.synced_at !== undefined ? rawResponse.synced_at : (rawResponse.since !== undefined ? rawResponse.since : null);
99
+ }
100
+
101
+ const normalizedData = rawRecords.map(r => self.mapper.ingress(r));
102
+ children.store.applySync(normalizedData, deletedIds, syncedAt);
103
+ })
104
+ .catch(function (err) {
105
+ console.error('[ln-data-coordinator] Sync failed:', err);
106
+ });
107
+ },
108
+
109
+ create: function (e) {
110
+ self.refreshMapper();
111
+ const children = self.findChildren();
112
+ if (!children.store || !children.connector) return;
113
+
114
+ const tempId = e.detail.tempId;
115
+ const data = e.detail.data || {};
116
+
117
+ // Apply egress mapping
118
+ const serverPayload = self.mapper.egress(data);
119
+
120
+ children.connector.create(serverPayload)
121
+ .then(function (serverRawResponse) {
122
+ const confirmedLocalRecord = self.mapper.ingress(serverRawResponse);
123
+ children.store.confirmMutation(tempId, confirmedLocalRecord, 'create');
124
+ })
125
+ .catch(function (err) {
126
+ console.error('[ln-data-coordinator] Create mutation failed:', err);
127
+ children.store.revertMutation(tempId, 'create', err.message || err);
128
+ });
129
+ },
130
+
131
+ update: function (e) {
132
+ self.refreshMapper();
133
+ const children = self.findChildren();
134
+ if (!children.store || !children.connector) return;
135
+
136
+ const id = e.detail.id;
137
+ const expectedVersion = e.detail.expected_version;
138
+
139
+ // Fetch complete merged local record to pass to egress for full PUT mapping
140
+ children.store.getById(id)
141
+ .then(function (localRecord) {
142
+ if (!localRecord) throw new Error('Record not found in cache store: ' + id);
143
+
144
+ const cleanRecord = Object.assign({}, localRecord);
145
+ delete cleanRecord._pending;
146
+
147
+ const serverPayload = self.mapper.egress(cleanRecord);
148
+
149
+ return children.connector.update(id, serverPayload, expectedVersion);
150
+ })
151
+ .then(function (serverRawResponse) {
152
+ const confirmedLocalRecord = self.mapper.ingress(serverRawResponse);
153
+ children.store.confirmMutation(id, confirmedLocalRecord, 'update');
154
+ })
155
+ .catch(function (err) {
156
+ console.error('[ln-data-coordinator] Update mutation failed:', err);
157
+ if (err.status === 409) {
158
+ const remoteRecord = err.data && err.data.remote ? self.mapper.ingress(err.data.remote) : null;
159
+ const fieldDiffs = err.data ? err.data.field_diffs : null;
160
+ children.store.resolveConflict(id, remoteRecord, fieldDiffs);
161
+ } else {
162
+ children.store.revertMutation(id, 'update', err.message || err);
163
+ }
164
+ });
165
+ },
166
+
167
+ delete: function (e) {
168
+ self.refreshMapper();
169
+ const children = self.findChildren();
170
+ if (!children.store || !children.connector) return;
171
+
172
+ const id = e.detail.id;
173
+
174
+ children.connector.delete(id)
175
+ .then(function () {
176
+ children.store.confirmMutation(id, null, 'delete');
177
+ })
178
+ .catch(function (err) {
179
+ console.error('[ln-data-coordinator] Delete mutation failed:', err);
180
+ children.store.revertMutation(id, 'delete', err.message || err);
181
+ });
182
+ },
183
+
184
+ bulkDelete: function (e) {
185
+ self.refreshMapper();
186
+ const children = self.findChildren();
187
+ if (!children.store || !children.connector) return;
188
+
189
+ const ids = e.detail.ids || [];
190
+ const bulkKey = ids.join(',');
191
+
192
+ children.connector.bulkDelete(ids)
193
+ .then(function () {
194
+ children.store.confirmMutation(bulkKey, null, 'bulk-delete');
195
+ })
196
+ .catch(function (err) {
197
+ console.error('[ln-data-coordinator] Bulk delete mutation failed:', err);
198
+ children.store.revertMutation(bulkKey, 'bulk-delete', err.message || err);
199
+ });
200
+ }
201
+ };
202
+
203
+ // Listen to all request-remote events bubbling up from the child store
204
+ self.dom.addEventListener('ln-store:request-remote-sync', self._handlers.sync);
205
+ self.dom.addEventListener('ln-store:request-remote-create', self._handlers.create);
206
+ self.dom.addEventListener('ln-store:request-remote-update', self._handlers.update);
207
+ self.dom.addEventListener('ln-store:request-remote-delete', self._handlers.delete);
208
+ self.dom.addEventListener('ln-store:request-remote-bulk-delete', self._handlers.bulkDelete);
209
+ }
210
+
211
+ // ─── Destroy and Cleanup ──────────────────────────────────
212
+
213
+ _component.prototype.destroy = function () {
214
+ if (!this.dom[DOM_ATTRIBUTE]) return;
215
+
216
+ const self = this;
217
+ if (self._handlers) {
218
+ self.dom.removeEventListener('ln-store:request-remote-sync', self._handlers.sync);
219
+ self.dom.removeEventListener('ln-store:request-remote-create', self._handlers.create);
220
+ self.dom.removeEventListener('ln-store:request-remote-update', self._handlers.update);
221
+ self.dom.removeEventListener('ln-store:request-remote-delete', self._handlers.delete);
222
+ self.dom.removeEventListener('ln-store:request-remote-bulk-delete', self._handlers.bulkDelete);
223
+ self._handlers = null;
224
+ }
225
+
226
+ delete this.dom[DOM_ATTRIBUTE];
227
+ delete this.dom[DOM_ALIAS];
228
+ };
229
+
230
+ // ─── Attribute Sync ────────────────────────────────────────
231
+
232
+ function _syncAttribute(el, attrName) {
233
+ const instance = el[DOM_ATTRIBUTE];
234
+ if (!instance) return;
235
+
236
+ if (attrName === 'data-ln-data-mapper') {
237
+ instance.refreshMapper();
238
+ }
239
+ }
240
+
241
+ // ─── Registration ──────────────────────────────────────
242
+
243
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-data-coordinator', {
244
+ extraAttributes: [
245
+ 'data-ln-data-mapper'
246
+ ],
247
+ onAttributeChange: _syncAttribute
248
+ });
249
+ })();
@@ -0,0 +1,94 @@
1
+ # ln-data-store
2
+
3
+ A zero-dependency, local-first **Database Cache Store** backed by standard browser `IndexedDB`. It acts as a pure client-side database cache, maintaining records locally, executing high-performance querying in-memory, and managing optimistic mutations with automatic snapshots for transaction safety.
4
+
5
+ It possesses no visual interface and is **completely blind to the network** (no fetch, status codes, paths, or urls). Instead, it communicates strictly via custom DOM events, allowing the parent **Data Coordinator** to orchestrate syncs and mutations.
6
+
7
+ ---
8
+
9
+ ## 📦 Declarative Setup in HTML
10
+
11
+ ```html
12
+ <div data-ln-data-store="documents"
13
+ data-ln-data-store-stale="300"
14
+ data-ln-data-store-indexes="status,department,updated_at"
15
+ data-ln-data-store-search-fields="title,owner">
16
+ </div>
17
+ ```
18
+
19
+ ---
20
+
21
+ ## 🛠️ JS API (On the element)
22
+
23
+ Access the database layer directly via the `lnDataStore` property on the store container:
24
+
25
+ ```javascript
26
+ const store = document.querySelector('[data-ln-data-store="documents"]');
27
+
28
+ // 1. Queries (returns Promise)
29
+ const { data, total, filtered } = await store.lnDataStore.getAll({
30
+ sort: { field: 'updated_at', direction: 'desc' },
31
+ filters: { status: ['Approved'] },
32
+ search: 'ISO 27001',
33
+ limit: 50
34
+ });
35
+
36
+ const doc = await store.lnDataStore.getById(42);
37
+
38
+ // 2. Decorators / Computed Fields Configuration
39
+ store.lnDataStore.setPresenters({
40
+ computed: {
41
+ size_display: record => (record.file_size / 1024).toFixed(1) + ' MB'
42
+ }
43
+ });
44
+ ```
45
+
46
+ ---
47
+
48
+ ## ⚡ DOM Events
49
+
50
+ ### Commands (Dispatched TO the store)
51
+
52
+ *Never invoke write methods directly.* Route all mutations through DOM commands:
53
+
54
+ ```javascript
55
+ // Create optimistically
56
+ store.dispatchEvent(new CustomEvent('ln-store:request-create', {
57
+ detail: { data: { title: 'New Document', status: 'Draft' } }
58
+ }));
59
+
60
+ // Update with version locks
61
+ store.dispatchEvent(new CustomEvent('ln-store:request-update', {
62
+ detail: { id: 42, data: { title: 'Updated title' }, expected_version: 3 }
63
+ }));
64
+ ```
65
+
66
+ ### Remote Request Events (Bubbles UP to the Coordinator)
67
+
68
+ *The Coordinator catches these events to run transport sync and mutations:*
69
+
70
+ * `ln-store:request-remote-sync` (detail: `{ since }`)
71
+ * `ln-store:request-remote-create` (detail: `{ tempId, data }`)
72
+ * `ln-store:request-remote-update` (detail: `{ id, data, expected_version }`)
73
+ * `ln-store:request-remote-delete` (detail: `{ id }`)
74
+ * `ln-store:request-remote-bulk-delete` (detail: `{ ids }`)
75
+
76
+ ---
77
+
78
+ ## 🚀 Public Synchronization APIs (Invoked by the Coordinator)
79
+
80
+ Once the network connector returns backend payloads, the Coordinator feeds the results back to the store using these public methods:
81
+
82
+ ```javascript
83
+ // Feed delta updates or initial load data
84
+ store.lnDataStore.applySync(upsertedRecords, deletedIds, syncedAt);
85
+
86
+ // Confirm an optimistic transaction (swaps temp IDs, clears pending flag)
87
+ store.lnDataStore.confirmMutation(tempIdOrId, serverRecord, action);
88
+
89
+ // Revert an optimistic transaction (restores snapshot on error)
90
+ store.lnDataStore.revertMutation(tempIdOrId, action, errorMessage);
91
+
92
+ // Trigger conflict merge flow
93
+ store.lnDataStore.resolveConflict(id, remoteRecord, fieldDiffs);
94
+ ```
@@ -0,0 +1 @@
1
+ (function(){"use strict";function l(a,i,d){a.dispatchEvent(new CustomEvent(i,{bubbles:!0,detail:d||{}}))}function H(a,i){if(!document.body){document.addEventListener("DOMContentLoaded",function(){H(a,i)}),console.warn("["+i+'] Script loaded before <body> — add "defer" to your <script> tag');return}a()}function K(a,i,d,h){if(a.nodeType!==1)return;const _=i.indexOf("[")!==-1||i.indexOf(".")!==-1||i.indexOf("#")!==-1?i:"["+i+"]",m=Array.from(a.querySelectorAll(_));a.matches&&a.matches(_)&&m.push(a);for(const u of m)u[d]||(u[d]=new h(u))}function ee(a,i,d,h,p={}){const _=p.extraAttributes||[],m=p.onAttributeChange||null,u=p.onInit||null;function g(L){const A=L||document.body;K(A,a,i,d),u&&u(A)}return H(function(){const L=new MutationObserver(function(k){for(let w=0;w<k.length;w++){const y=k[w];if(y.type==="childList")for(let B=0;B<y.addedNodes.length;B++){const R=y.addedNodes[B];R.nodeType===1&&(K(R,a,i,d),u&&u(R))}else y.type==="attributes"&&(m&&y.target[i]?m(y.target,y.attributeName):(K(y.target,a,i,d),u&&u(y.target)))}});let A=[];if(a.indexOf("[")!==-1){const k=/\[([\w-]+)/g;let w;for(;(w=k.exec(a))!==null;)A.push(w[1])}else A.push(a);L.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:A.concat(_)})},h),window[i]=g,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){g(document.body)}):g(document.body),g}const V={};function te(a,i){V[a]=i}function ne(a){return V[a]||{ingress:i=>i,egress:i=>i}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=te,window.lnCore.getDataMapper=ne);let x=null;async function G(a){if(!a){x=null;return}try{const i=new TextEncoder,d=await crypto.subtle.digest("SHA-256",i.encode(a));x=await crypto.subtle.importKey("raw",d,{name:"AES-GCM"},!1,["encrypt","decrypt"])}catch(i){console.error("[ln-core/crypto] Key derivation failed:",i),x=null}}function j(){return x}async function re(a,i=x){const d=i||x;if(!d||a===void 0||a===null)return a;try{const h=new TextEncoder,p=crypto.getRandomValues(new Uint8Array(12)),_=typeof a=="string"?a:JSON.stringify(a),m=await crypto.subtle.encrypt({name:"AES-GCM",iv:p},d,h.encode(_)),u=btoa(String.fromCharCode(...p)),g=btoa(String.fromCharCode(...new Uint8Array(m)));return{encrypted:!0,iv:u,data:g}}catch(h){return console.error("[ln-core/crypto] Encryption failed:",h),a}}async function oe(a,i=x){const d=i||x;if(!a||!a.encrypted||!d)return a;try{const h=new TextDecoder,p=Uint8Array.from(atob(a.iv),g=>g.charCodeAt(0)),_=Uint8Array.from(atob(a.data),g=>g.charCodeAt(0)),m=await crypto.subtle.decrypt({name:"AES-GCM",iv:p},d,_),u=h.decode(m);try{return JSON.parse(u)}catch{return u}}catch(h){return console.error("[ln-core/crypto] Decryption failed. Key may be incorrect:",h),{...a,decryptionError:!0}}}(function(){const a="data-ln-data-store",i="lnDataStore";if(window[i]!==void 0)return;const d="ln_app_cache",h="_meta",p="1.0";let _=null,m=null;const u={};function g(){try{return crypto.randomUUID()}catch{return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const r=Math.random()*16|0;return(t==="x"?r:r&3|8).toString(16)})}}function L(e){e&&e.name==="QuotaExceededError"&&l(document,"ln-store:quota-exceeded",{error:e})}function A(){const e={};for(const t of document.querySelectorAll(`[${a}]`)){const r=t.getAttribute(a);if(r){const n=t.getAttribute("data-ln-data-store-indexes")||t.getAttribute("data-ln-store-indexes")||"";e[r]={indexes:n.split(",").map(o=>o.trim()).filter(Boolean)}}}return e}function k(){return m||(m=new Promise(e=>{if(typeof indexedDB>"u")return console.warn("[ln-data-store] IndexedDB not available — falling back to in-memory store"),e(null);const t=A(),r=Object.keys(t),n=indexedDB.open(d);n.onerror=()=>{console.warn("[ln-data-store] IndexedDB open failed — falling back to in-memory store"),e(null)},n.onsuccess=o=>{const s=o.target.result,c=Array.from(s.objectStoreNames);if(!(!c.includes(h)||r.some(q=>!c.includes(q))))return w(s),_=s,e(s);const S=s.version;s.close();const v=indexedDB.open(d,S+1);v.onblocked=()=>{console.warn("[ln-data-store] Database upgrade blocked — waiting for other tabs to close connection")},v.onerror=()=>{console.warn("[ln-data-store] Database upgrade failed"),e(null)},v.onupgradeneeded=q=>{const P=q.target.result;P.objectStoreNames.contains(h)||P.createObjectStore(h,{keyPath:"key"});for(const $ of r)if(!P.objectStoreNames.contains($)){const ye=P.createObjectStore($,{keyPath:"id"});for(const I of t[$].indexes)ye.createIndex(I,I,{unique:!1})}},v.onsuccess=q=>{const P=q.target.result;w(P),_=P,e(P)}}}),m)}function w(e){e.onversionchange=()=>{e.close(),_=null,m=null}}function y(){return _?Promise.resolve(_):(m=null,k())}async function B(e){if(!j()||!e)return e;const t={...e},r=t.id,n=t._pending,o=await re(t);return!o||!o.encrypted?e:{id:r,_pending:n,encrypted:!0,iv:o.iv,data:o.data}}async function R(e){return!e||!e.encrypted||!j()?e:oe(e)}const C=(e,t)=>y().then(r=>r?r.transaction(e,t).objectStore(e):null);function E(e){return new Promise((t,r)=>{e.onsuccess=()=>t(e.result),e.onerror=()=>{L(e.error),r(e.error)}})}const U=e=>C(e,"readonly").then(t=>t?E(t.getAll()):[]).then(t=>j()?Promise.all(t.map(r=>R(r))):t),N=(e,t)=>C(e,"readonly").then(r=>r?E(r.get(t)):null).then(r=>r?R(r):null),D=(e,t)=>(j()?B(t):Promise.resolve(t)).then(n=>C(e,"readwrite").then(o=>o?E(o.put(n)):null)),O=(e,t)=>C(e,"readwrite").then(r=>r?E(r.delete(t)):null),J=e=>C(e,"readwrite").then(t=>t?E(t.clear()):null),Q=e=>C(e,"readonly").then(t=>t?E(t.count()):0),se=e=>C(h,"readonly").then(t=>t?E(t.get(e)):null),z=(e,t)=>C(h,"readwrite").then(r=>{if(r)return t.key=e,E(r.put(t))});function b(e){this.dom=e,this._name=e.getAttribute(a);const t=e.getAttribute("data-ln-data-store-stale")||e.getAttribute("data-ln-store-stale"),r=parseInt(t,10);this._staleThreshold=t==="never"||t==="-1"?-1:isNaN(r)?300:r;const n=e.getAttribute("data-ln-data-store-search-fields")||e.getAttribute("data-ln-store-search-fields")||"";return this._searchFields=n.split(",").map(o=>o.trim()).filter(Boolean),this._handlers=null,this._pendingSnapshots={},this.isLoaded=!1,this.isSyncing=!1,this.lastSyncedAt=null,this.totalCount=0,this.presenters=null,u[this._name]=this,ae(this),de(this),this}function ae(e){e._handlers={create:t=>ie(e,t.detail),update:t=>ce(e,t.detail),delete:t=>le(e,t.detail),"bulk-delete":t=>ue(e,t.detail)};for(const[t,r]of Object.entries(e._handlers))e.dom.addEventListener(`ln-store:request-${t}`,r)}function ie(e,{data:t={}}={}){const r=`_temp_${g()}`,n={...t,id:r,_pending:!0};D(e._name,n).then(()=>{e.totalCount++,l(e.dom,"ln-store:created",{store:e._name,record:n,tempId:r}),l(e.dom,"ln-store:request-remote-create",{tempId:r,data:t})})}function ce(e,{id:t,data:r={},expected_version:n}={}){N(e._name,t).then(o=>{if(!o)throw new Error(`Record not found: ${t}`);e._pendingSnapshots[t]={...o};const s={...o,...r,_pending:!0};return D(e._name,s).then(()=>{l(e.dom,"ln-store:updated",{store:e._name,record:s,previous:e._pendingSnapshots[t]}),l(e.dom,"ln-store:request-remote-update",{id:t,data:r,expected_version:n})})}).catch(o=>console.error("[ln-data-store] Optimistic update failed:",o))}function le(e,{id:t}={}){N(e._name,t).then(r=>{if(r)return e._pendingSnapshots[t]={...r},O(e._name,t).then(()=>{e.totalCount--,l(e.dom,"ln-store:deleted",{store:e._name,id:t}),l(e.dom,"ln-store:request-remote-delete",{id:t})})}).catch(r=>console.error("[ln-data-store] Optimistic delete failed:",r))}function ue(e,{ids:t=[]}={}){t.length&&Promise.all(t.map(r=>N(e._name,r))).then(r=>{const n=r.filter(Boolean),o=n.map(s=>s.id);return e._pendingSnapshots[o.join(",")]=n,Y(e._name,o).then(()=>{e.totalCount-=o.length,l(e.dom,"ln-store:deleted",{store:e._name,ids:o}),l(e.dom,"ln-store:request-remote-bulk-delete",{ids:o})})}).catch(r=>console.error("[ln-data-store] Optimistic bulk delete failed:",r))}function de(e){k().then(()=>se(e._name)).then(t=>{t&&t.schema_version===p?(e.lastSyncedAt=t.last_synced_at||null,e.totalCount=t.record_count||0,e.totalCount>0?(e.isLoaded=!0,l(e.dom,"ln-store:ready",{store:e._name,count:e.totalCount,source:"cache"}),W(e)&&M(e)):M(e)):t&&t.schema_version!==p?J(e._name).then(()=>z(e._name,{schema_version:p,last_synced_at:null,record_count:0})).then(()=>M(e)):M(e)})}function W(e){return e._staleThreshold===-1?!1:e.lastSyncedAt?Math.floor(Date.now()/1e3)-e.lastSyncedAt>e._staleThreshold:!0}function M(e){e.isSyncing=!0,l(e.dom,"ln-store:request-remote-sync",{since:e.lastSyncedAt})}function X(e,t){return y().then(r=>r?(j()?Promise.all(t.map(o=>B(o))):Promise.resolve(t)).then(o=>new Promise((s,c)=>{const f=r.transaction(e,"readwrite"),S=f.objectStore(e);o.forEach(v=>S.put(v)),f.oncomplete=()=>s(),f.onerror=()=>{L(f.error),c(f.error)}})):void 0)}function Y(e,t){return y().then(r=>{if(r)return new Promise((n,o)=>{const s=r.transaction(e,"readwrite"),c=s.objectStore(e);t.forEach(f=>c.delete(f)),s.oncomplete=()=>n(),s.onerror=()=>o(s.error)})})}let T=()=>{document.visibilityState==="visible"&&Object.values(u).forEach(e=>{e.isLoaded&&!e.isSyncing&&W(e)&&M(e)})};document.addEventListener("visibilitychange",T);const he=new Intl.Collator(void 0,{numeric:!0,sensitivity:"base"});function fe(e,t){if(!t||!t.field)return e;const{field:r,direction:n}=t,o=n==="desc";return[...e].sort((s,c)=>{const f=s[r],S=c[r];if(f==null&&S==null)return 0;if(f==null)return o?1:-1;if(S==null)return o?-1:1;const v=typeof f=="string"&&typeof S=="string"?he.compare(f,S):f<S?-1:f>S?1:0;return o?-v:v})}function Z(e,t){if(!t)return e;const r=Object.keys(t).filter(n=>Array.isArray(t[n])&&t[n].length>0);return r.length?e.filter(n=>r.every(o=>t[o].map(String).includes(String(n[o])))):e}function me(e,t,r){if(!t||!r||!r.length)return e;const n=t.toLowerCase();return e.filter(o=>r.some(s=>{const c=o[s];return c!=null&&String(c).toLowerCase().includes(n)}))}function pe(e,t,r){if(!e.length)return 0;if(r==="count")return e.length;const n=e.map(s=>parseFloat(s[t])).filter(s=>!isNaN(s)),o=n.reduce((s,c)=>s+c,0);return r==="sum"?o:r==="avg"&&n.length?o/n.length:0}function F(e,t){if(!e.presenters||!e.presenters.computed)return t;const r=e.presenters.computed;return t.map(n=>{const o={...n};for(const[s,c]of Object.entries(r))try{o[s]=c(n)}catch(f){console.error(`[ln-data-store] Decorator computed field failed for ${s}`,f)}return o})}b.prototype.getAll=function(e={}){const t=this;return U(t._name).then(r=>{const n=r.length;e.filters&&(r=Z(r,e.filters)),e.search&&(r=me(r,e.search,t._searchFields));const o=r.length;if(e.sort&&(r=fe(r,e.sort)),e.offset||e.limit){const s=e.offset||0,c=e.limit||r.length;r=r.slice(s,s+c)}return{data:F(t,r),total:n,filtered:o}})},b.prototype.getById=function(e){return N(this._name,e).then(t=>t?F(this,[t])[0]:null)},b.prototype.count=function(e){return e?U(this._name).then(t=>Z(t,e).length):Q(this._name)},b.prototype.aggregate=function(e,t){return U(this._name).then(r=>pe(r,e,t))},b.prototype.setPresenters=function(e){this.presenters=e},b.prototype.applySync=function(e,t,r){const n=this,o=e.length>0||t.length>0;let s=Promise.resolve();return e.length>0&&(s=s.then(()=>X(n._name,e))),t.length>0&&(s=s.then(()=>Y(n._name,t))),s.then(()=>Q(n._name)).then(c=>(n.totalCount=c,z(n._name,{schema_version:p,last_synced_at:r,record_count:c}))).then(()=>{const c=!n.isLoaded;n.isLoaded=!0,n.isSyncing=!1,n.lastSyncedAt=r,c?(l(n.dom,"ln-store:loaded",{store:n._name,count:n.totalCount}),l(n.dom,"ln-store:ready",{store:n._name,count:n.totalCount,source:"server"})):l(n.dom,"ln-store:synced",{store:n._name,added:e.length,deleted:t.length,changed:o})}).catch(c=>{n.isSyncing=!1,console.error("[ln-data-store] applySync failed:",c)})},b.prototype.confirmMutation=function(e,t,r){const n=this,o={create:()=>O(n._name,e).then(()=>D(n._name,t)).then(()=>{delete n._pendingSnapshots[e],l(n.dom,"ln-store:confirmed",{store:n._name,record:t,tempId:e,action:"create"})}),update:()=>D(n._name,t).then(()=>{delete n._pendingSnapshots[e],l(n.dom,"ln-store:confirmed",{store:n._name,record:t,action:"update"})}),delete:()=>(delete n._pendingSnapshots[e],l(n.dom,"ln-store:confirmed",{store:n._name,record:null,action:"delete"}),Promise.resolve()),"bulk-delete":()=>(delete n._pendingSnapshots[e],l(n.dom,"ln-store:confirmed",{store:n._name,record:null,ids:e.split(","),action:"bulk-delete"}),Promise.resolve())};return o[r]?o[r]():Promise.resolve()},b.prototype.revertMutation=function(e,t,r){const n=this,o=r||`Server rejected ${t}`,s={create:()=>O(n._name,e).then(()=>{n.totalCount--,delete n._pendingSnapshots[e],l(n.dom,"ln-store:reverted",{store:n._name,record:null,action:"create",error:o})}),update:()=>{const c=n._pendingSnapshots[e];return c?D(n._name,c).then(()=>{delete n._pendingSnapshots[e],l(n.dom,"ln-store:reverted",{store:n._name,record:c,action:"update",error:o})}):Promise.resolve()},delete:()=>{const c=n._pendingSnapshots[e];return c?D(n._name,c).then(()=>{n.totalCount++,delete n._pendingSnapshots[e],l(n.dom,"ln-store:reverted",{store:n._name,record:c,action:"delete",error:o})}):Promise.resolve()},"bulk-delete":()=>{const c=n._pendingSnapshots[e];return!c||!c.length?Promise.resolve():X(n._name,c).then(()=>{n.totalCount+=c.length,delete n._pendingSnapshots[e],l(n.dom,"ln-store:reverted",{store:n._name,record:null,ids:e.split(","),action:"bulk-delete",error:o})})}};return s[t]?s[t]():Promise.resolve()},b.prototype.resolveConflict=function(e,t,r){const n=this._pendingSnapshots[e];return n?D(this._name,n).then(()=>{delete this._pendingSnapshots[e],l(this.dom,"ln-store:conflict",{store:this._name,local:n,remote:t,field_diffs:r||null})}):Promise.resolve()},b.prototype.forceSync=function(){M(this)},b.prototype.fullReload=function(){const e=this;return J(e._name).then(()=>{e.isLoaded=!1,e.lastSyncedAt=null,e.totalCount=0,M(e)})},b.prototype.destroy=function(){if(this._handlers){for(const[e,t]of Object.entries(this._handlers))this.dom.removeEventListener(`ln-store:request-${e}`,t);this._handlers=null}delete u[this._name],Object.keys(u).length===0&&T&&(document.removeEventListener("visibilitychange",T),T=null),delete this.dom[i],l(this.dom,"ln-store:destroyed",{store:this._name})};function _e(){return y().then(e=>{if(!e)return;const t=Array.from(e.objectStoreNames);return new Promise((r,n)=>{const o=e.transaction(t,"readwrite");t.forEach(s=>o.objectStore(s).clear()),o.oncomplete=()=>r(),o.onerror=()=>n(o.error)})}).then(()=>{Object.values(u).forEach(e=>{e.isLoaded=!1,e.isSyncing=!1,e.lastSyncedAt=null,e.totalCount=0})})}ee(a,i,b,"ln-data-store"),window[i].clearAll=_e,window[i].init=window[i],window[i].setStorageKey=G,typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.setStorageKey=G)})()})();