@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,348 @@
1
+ import { registerComponent, dispatch, buildUrl, getHeaders, parseHeaders } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-couchdb-connector';
5
+ const DOM_ATTRIBUTE = 'lnCouchDbConnector';
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; // Alias for 3-tier 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.url = dom.getAttribute('data-ln-couchdb-url') || '';
30
+ this.db = dom.getAttribute('data-ln-couchdb-db') || '';
31
+ this.auth = dom.getAttribute('data-ln-couchdb-auth') || '';
32
+ this.credentials = 'same-origin';
33
+
34
+ const rawHeaders = dom.getAttribute('data-ln-couchdb-headers') || '';
35
+ this.headers = parseHeaders(rawHeaders, 'ln-couchdb-connector');
36
+
37
+ if (this.auth) {
38
+ console.warn('[ln-couchdb-connector] Security Warning: Sensitive authorization credentials detected in data-ln-couchdb-auth attribute. Storing basic authentication credentials in HTML DOM attributes is highly discouraged and vulnerable to XSS credential extraction. Please use HttpOnly session cookies or a Backend Proxy Gateway instead.');
39
+ }
40
+ if (rawHeaders.toLowerCase().includes('authorization')) {
41
+ console.warn('[ln-couchdb-connector] Security Warning: Sensitive authorization credentials detected in data-ln-couchdb-headers attribute. Please use HttpOnly session cookies or a Backend Proxy Gateway instead.');
42
+ }
43
+
44
+ dispatch(dom, 'ln-couchdb-connector:config-changed', {
45
+ url: this.url,
46
+ db: this.db,
47
+ auth: this.auth ? '[REDACTED]' : '',
48
+ headers: this.headers
49
+ });
50
+ };
51
+
52
+ // ─── CouchDB REST API Methods (Promises) ────────────────
53
+
54
+ /**
55
+ * Fetch changed records since a sequence.
56
+ * Calls CouchDB /{db}/_changes?include_docs=true&since={since}
57
+ */
58
+ _component.prototype.fetchDelta = function (since) {
59
+ const self = this;
60
+ const params = ['include_docs=true', 'feed=normal'];
61
+ if (since) params.push('since=' + encodeURIComponent(since));
62
+ const url = buildUrl(self.url, self.db, '_changes') + '?' + params.join('&');
63
+
64
+ return window.fetch(url, { method: 'GET', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
65
+ .then(res => {
66
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
67
+ return res.json();
68
+ })
69
+ .then(data => {
70
+ const results = data.results || [];
71
+ return {
72
+ data: results.filter(r => !r.deleted && r.doc).map(r => Object.assign({}, r.doc, { id: r.doc._id })),
73
+ deleted: results.filter(r => r.deleted).map(r => r.id),
74
+ synced_at: data.last_seq || since || ''
75
+ };
76
+ });
77
+ };
78
+
79
+ /**
80
+ * Create a document in CouchDB.
81
+ * Sends POST /{db}
82
+ */
83
+ _component.prototype.create = function (payload) {
84
+ const self = this;
85
+ const doc = Object.assign({ _id: payload.id }, payload);
86
+ if (!doc._id) delete doc._id;
87
+
88
+ return window.fetch(buildUrl(self.url, self.db), {
89
+ method: 'POST',
90
+ headers: getHeaders(self.headers, self.auth),
91
+ credentials: self.credentials,
92
+ body: JSON.stringify(doc)
93
+ })
94
+ .then(res => {
95
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
96
+ return res.json();
97
+ })
98
+ .then(resData => Object.assign({}, doc, { id: resData.id, _id: resData.id, _rev: resData.rev }));
99
+ };
100
+
101
+ /**
102
+ * Update a document in CouchDB.
103
+ * Sends PUT /{db}/{id}
104
+ * Handles revision checks and automatic revision fetching if rev is missing.
105
+ */
106
+ _component.prototype.update = function (id, payload) {
107
+ const self = this;
108
+ const doc = Object.assign({ id: String(id), _id: String(id) }, payload);
109
+ const rev = doc._rev || doc.rev;
110
+
111
+ const getRevPromise = rev ? Promise.resolve(rev) :
112
+ window.fetch(buildUrl(self.url, self.db, null, id), { method: 'GET', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
113
+ .then(res => {
114
+ if (!res.ok) throw new Error('Could not retrieve document for revision mapping');
115
+ return res.json().then(d => d._rev);
116
+ });
117
+
118
+ return getRevPromise.then(activeRev => {
119
+ const finalDoc = Object.assign({}, doc, { _rev: activeRev });
120
+ delete finalDoc.rev;
121
+
122
+ const headers = Object.assign(getHeaders(self.headers, self.auth), { 'If-Match': activeRev });
123
+ return window.fetch(buildUrl(self.url, self.db, null, id), {
124
+ method: 'PUT',
125
+ headers: headers,
126
+ credentials: self.credentials,
127
+ body: JSON.stringify(finalDoc)
128
+ })
129
+ .then(res => {
130
+ if (res.ok) return res.json().then(data => Object.assign({}, finalDoc, { _rev: data.rev }));
131
+ if (res.status === 409) return res.json().then(data => {
132
+ const err = new Error('Conflict');
133
+ err.status = 409;
134
+ err.data = data;
135
+ throw err;
136
+ });
137
+ throw new Error('HTTP ' + res.status + ': ' + res.statusText);
138
+ });
139
+ });
140
+ };
141
+
142
+ /**
143
+ * Delete a document in CouchDB.
144
+ * Sends DELETE /{db}/{id}?rev={rev}
145
+ */
146
+ _component.prototype.delete = function (id, rev) {
147
+ const self = this;
148
+ const getRevPromise = rev ? Promise.resolve(rev) :
149
+ window.fetch(buildUrl(self.url, self.db, null, id), { method: 'GET', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
150
+ .then(res => {
151
+ if (!res.ok) throw new Error('Could not retrieve document for revision delete');
152
+ return res.json().then(d => d._rev);
153
+ });
154
+
155
+ return getRevPromise.then(activeRev => {
156
+ const deleteUrl = buildUrl(self.url, self.db, null, id) + '?rev=' + encodeURIComponent(activeRev);
157
+ return window.fetch(deleteUrl, { method: 'DELETE', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
158
+ .then(res => {
159
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
160
+ return res.json();
161
+ });
162
+ });
163
+ };
164
+
165
+ /**
166
+ * Bulk delete documents in CouchDB.
167
+ * Sends POST /{db}/_bulk_docs with {"docs": [{"_id": "...", "_rev": "...", "_deleted": true}]}
168
+ */
169
+ _component.prototype.bulkDelete = function (ids) {
170
+ const self = this;
171
+ if (!ids || ids.length === 0) return Promise.resolve({ ok: true, deletedCount: 0 });
172
+
173
+ return window.fetch(buildUrl(self.url, self.db, '_all_docs'), {
174
+ method: 'POST',
175
+ headers: getHeaders(self.headers, self.auth),
176
+ credentials: self.credentials,
177
+ body: JSON.stringify({ keys: ids })
178
+ })
179
+ .then(res => {
180
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
181
+ return res.json();
182
+ })
183
+ .then(data => {
184
+ const rows = data.rows || [];
185
+ const docsToDelete = rows
186
+ .filter(r => !r.error && r.value && r.value.rev)
187
+ .map(r => ({ _id: r.id, _rev: r.value.rev, _deleted: true }));
188
+
189
+ if (docsToDelete.length === 0) return { ok: true, deletedCount: 0 };
190
+
191
+ return window.fetch(buildUrl(self.url, self.db, '_bulk_docs'), {
192
+ method: 'POST',
193
+ headers: getHeaders(self.headers, self.auth),
194
+ credentials: self.credentials,
195
+ body: JSON.stringify({ docs: docsToDelete })
196
+ })
197
+ .then(res => {
198
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
199
+ return res.json();
200
+ })
201
+ .then(bulkData => ({ ok: true, results: bulkData, deletedCount: docsToDelete.length }));
202
+ });
203
+ };
204
+
205
+ // ─── DOM Event Routing ────────────────────────────────────
206
+
207
+ function _bindEvents(self) {
208
+ self._handlers = {
209
+ sync: function (e) {
210
+ const detail = e.detail || {};
211
+ self.fetchDelta(detail.since)
212
+ .then(function (data) {
213
+ dispatch(self.dom, 'ln-couchdb-connector:fetched', { data: data, since: detail.since });
214
+ })
215
+ .catch(function (err) {
216
+ dispatch(self.dom, 'ln-couchdb-connector:error', {
217
+ action: 'sync',
218
+ error: err.message,
219
+ status: err.status || 0,
220
+ since: detail.since
221
+ });
222
+ });
223
+ },
224
+ create: function (e) {
225
+ const detail = e.detail || {};
226
+ self.create(detail.data)
227
+ .then(function (record) {
228
+ dispatch(self.dom, 'ln-couchdb-connector:created', { record: record, tempId: detail.tempId });
229
+ })
230
+ .catch(function (err) {
231
+ dispatch(self.dom, 'ln-couchdb-connector:error', {
232
+ action: 'create',
233
+ error: err.message,
234
+ status: err.status || 0,
235
+ tempId: detail.tempId
236
+ });
237
+ });
238
+ },
239
+ update: function (e) {
240
+ const detail = e.detail || {};
241
+ const payload = Object.assign({}, detail.data);
242
+
243
+ // Handle expected revision details
244
+ if (detail.expected_version !== undefined) {
245
+ payload._rev = detail.expected_version;
246
+ }
247
+
248
+ self.update(detail.id, payload)
249
+ .then(function (record) {
250
+ dispatch(self.dom, 'ln-couchdb-connector:updated', { record: record, id: detail.id });
251
+ })
252
+ .catch(function (err) {
253
+ dispatch(self.dom, 'ln-couchdb-connector:error', {
254
+ action: 'update',
255
+ error: err.message,
256
+ status: err.status || 0,
257
+ id: detail.id,
258
+ conflictData: err.status === 409 ? err.data : null
259
+ });
260
+ });
261
+ },
262
+ delete: function (e) {
263
+ const detail = e.detail || {};
264
+ self.delete(detail.id, detail.rev)
265
+ .then(function (res) {
266
+ dispatch(self.dom, 'ln-couchdb-connector:deleted', { response: res, id: detail.id });
267
+ })
268
+ .catch(function (err) {
269
+ dispatch(self.dom, 'ln-couchdb-connector:error', {
270
+ action: 'delete',
271
+ error: err.message,
272
+ status: err.status || 0,
273
+ id: detail.id
274
+ });
275
+ });
276
+ },
277
+ bulkDelete: function (e) {
278
+ const detail = e.detail || {};
279
+ self.bulkDelete(detail.ids)
280
+ .then(function (res) {
281
+ dispatch(self.dom, 'ln-couchdb-connector:bulk-deleted', { response: res, ids: detail.ids });
282
+ })
283
+ .catch(function (err) {
284
+ dispatch(self.dom, 'ln-couchdb-connector:error', {
285
+ action: 'bulk-delete',
286
+ error: err.message,
287
+ status: err.status || 0,
288
+ ids: detail.ids
289
+ });
290
+ });
291
+ }
292
+ };
293
+
294
+ // Bind events for CouchDB namespaces and also REST/API connector namespaces for 3-tier compatibility
295
+ const namespaces = ['ln-couchdb-connector', 'ln-api-connector', 'ln-rest-connector'];
296
+ namespaces.forEach(function (ns) {
297
+ self.dom.addEventListener(ns + ':request-sync', self._handlers.sync);
298
+ self.dom.addEventListener(ns + ':request-fetch', self._handlers.sync);
299
+ self.dom.addEventListener(ns + ':request-create', self._handlers.create);
300
+ self.dom.addEventListener(ns + ':request-update', self._handlers.update);
301
+ self.dom.addEventListener(ns + ':request-delete', self._handlers.delete);
302
+ self.dom.addEventListener(ns + ':request-bulk-delete', self._handlers.bulkDelete);
303
+ });
304
+ }
305
+
306
+ _component.prototype.destroy = function () {
307
+ if (!this.dom[DOM_ATTRIBUTE]) return;
308
+
309
+ const self = this;
310
+ if (self._handlers) {
311
+ const namespaces = ['ln-couchdb-connector', 'ln-api-connector', 'ln-rest-connector'];
312
+ namespaces.forEach(function (ns) {
313
+ self.dom.removeEventListener(ns + ':request-sync', self._handlers.sync);
314
+ self.dom.removeEventListener(ns + ':request-fetch', self._handlers.sync);
315
+ self.dom.removeEventListener(ns + ':request-create', self._handlers.create);
316
+ self.dom.removeEventListener(ns + ':request-update', self._handlers.update);
317
+ self.dom.removeEventListener(ns + ':request-delete', self._handlers.delete);
318
+ self.dom.removeEventListener(ns + ':request-bulk-delete', self._handlers.bulkDelete);
319
+ });
320
+ self._handlers = null;
321
+ }
322
+
323
+ dispatch(this.dom, 'ln-couchdb-connector:destroyed', { target: this.dom });
324
+
325
+ delete this.dom[DOM_ATTRIBUTE];
326
+ delete this.dom[DOM_ALIAS];
327
+ };
328
+
329
+ // ─── Attribute Sync ────────────────────────────────────────
330
+
331
+ function _syncAttribute(el) {
332
+ const instance = el[DOM_ATTRIBUTE];
333
+ if (!instance) return;
334
+ instance.refreshConfig();
335
+ }
336
+
337
+ // ─── Registration ──────────────────────────────────────
338
+
339
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-couchdb-connector', {
340
+ extraAttributes: [
341
+ 'data-ln-couchdb-url',
342
+ 'data-ln-couchdb-db',
343
+ 'data-ln-couchdb-auth',
344
+ 'data-ln-couchdb-headers'
345
+ ],
346
+ onAttributeChange: _syncAttribute
347
+ });
348
+ })();
@@ -0,0 +1,165 @@
1
+ # `data-ln-data-coordinator`
2
+
3
+ A zero-dependency, Local-First **Data Coordinator** component that acts as the orchestrating brain of the 3-Tier Data Layer in `ln-ashlar`.
4
+
5
+ This component monitors its DOM subtree, intercepts events, and coordinates the life cycle between a **Local Storage Cache** (`data-ln-data-store`) and any **Transport Gateway** (`data-ln-*-connector`). By orchestrating the flow using event bubbling and a robust Promise-driven chain, it implements the decoupled coordinator pattern, making caching and networking completely agnostic of each other.
6
+
7
+ ---
8
+
9
+ ## 🧭 The 3-Tier Data Layer Anatomy
10
+
11
+ The coordinator acts as a parent wrapper enclosing the database cache and transport connector:
12
+
13
+ ```html
14
+ <div data-ln-data-coordinator="documents"
15
+ data-ln-data-mapper="documents">
16
+
17
+ <!-- Tier 1: Local Cache Database (IndexedDB - pure and network-blind) -->
18
+ <div data-ln-data-store
19
+ data-ln-store-indexes="status,updated_at">
20
+ </div>
21
+
22
+ <!-- Tier 2: Transport Gateway (API / REST Connector) -->
23
+ <div data-ln-api-connector
24
+ data-ln-api-base-url="https://api.livenetworks.com/v1"
25
+ data-ln-api-path="/documents">
26
+ </div>
27
+ </div>
28
+ ```
29
+
30
+ ---
31
+
32
+ ## ⚙️ Attributes
33
+
34
+ | Attribute | Category | Description |
35
+ |-----------|----------|-------------|
36
+ | `data-ln-data-coordinator` | Selector | Creates the coordinator instance. The value acts as the domain/scope name (e.g. `documents`). |
37
+ | `data-ln-data-mapper` | Mapping | Reference to an externally registered data mapper name (e.g. `documents`). |
38
+
39
+ ---
40
+
41
+ ## 🔄 Dynamic Child & Mapper Discovery
42
+
43
+ The coordinator is built to be highly dynamic, reacting to runtime modifications in its DOM subtree.
44
+
45
+ 1. **Child Discovery**: The coordinator automatically locates its child components by querying its DOM subtree:
46
+ * **Store Cache**: Looks for `[data-ln-data-store]` and accesses `el.lnDataStore || el.lnStore`.
47
+ * **Transport Connector**: Looks for any connector selector (`[data-ln-api-connector]`, `[data-ln-couchdb-connector]`, `[data-ln-websocket-connector]`, `[data-ln-rest-connector]`) and accesses the universal alias `el.lnConnector`.
48
+
49
+ 2. **Mapper Resolution**: The coordinator resolves mapping functions using two strategies:
50
+ * **Inline Script (Highly Encapsulated)**: Looks for a nested `<script type="application/javascript" data-ln-mapper>` tag in its subtree:
51
+ ```html
52
+ <script type="application/javascript" data-ln-mapper>
53
+ ({
54
+ ingress(serverRaw) {
55
+ return {
56
+ id: serverRaw.id,
57
+ title: serverRaw.title,
58
+ status: serverRaw.status,
59
+ updated_at: Date.parse(serverRaw.updated_at) / 1000
60
+ };
61
+ },
62
+ egress(localDb) {
63
+ return {
64
+ title: localDb.title,
65
+ status: localDb.status
66
+ };
67
+ }
68
+ })
69
+ </script>
70
+ ```
71
+ * **External Registry (Reusability)**: If no inline script exists, it reads the `data-ln-data-mapper` attribute and looks up the mapper via `window.lnCore.getDataMapper(name)`.
72
+ * **Fallback**: Defaults to a safe no-op mapper: `{ ingress: r => r, egress: r => r }`.
73
+
74
+ ---
75
+
76
+ ## ⚡ The Event Loop Orchestration
77
+
78
+ Because all events dispatched by the child components bubble up, the coordinator listens directly on its own DOM boundary. It manages the following flows seamlessly:
79
+
80
+ ### 1. Delta & Full Sync (`ln-store:request-remote-sync`)
81
+ Triggered when the store cache boots up or detects stale data:
82
+ ```mermaid
83
+ sequenceDiagram
84
+ participant Store as data-ln-data-store
85
+ participant Coord as data-ln-data-coordinator
86
+ participant Gateway as data-ln-*-connector
87
+
88
+ Store->>Coord: Event: ln-store:request-remote-sync (since)
89
+ Coord->>Gateway: JS API: connector.fetchDelta(since)
90
+ Gateway-->>Coord: Promise: returns serverRawResponse
91
+ Note over Coord: Applies Ingress Mapper:<br/>mapper.ingress(serverRawRecord)
92
+ Coord->>Store: JS API: store.applySync(normalizedData, deletedIds, syncedAt)
93
+ ```
94
+
95
+ ### 2. Optimistic Creation (`ln-store:request-remote-create`)
96
+ Triggered when a form submits a new record to the local cache:
97
+ ```mermaid
98
+ sequenceDiagram
99
+ participant Store as data-ln-data-store
100
+ participant Coord as data-ln-data-coordinator
101
+ participant Gateway as data-ln-*-connector
102
+
103
+ Store->>Coord: Event: ln-store:request-remote-create (tempId, data)
104
+ Note over Coord: Applies Egress Mapper:<br/>mapper.egress(localRecord)
105
+ Coord->>Gateway: JS API: connector.create(serverPayload)
106
+ alt Server Success
107
+ Gateway-->>Coord: Promise: returns serverRawResponse
108
+ Note over Coord: Applies Ingress Mapper:<br/>mapper.ingress(serverRaw)
109
+ Coord->>Store: JS API: store.confirmMutation(tempId, confirmedLocalRecord, 'create')
110
+ else Server Failure
111
+ Gateway-->>Coord: Promise: Rejects
112
+ Coord->>Store: JS API: store.revertMutation(tempId, 'create', errorMessage)
113
+ end
114
+ ```
115
+
116
+ ### 3. Optimistic Updates (`ln-store:request-remote-update`)
117
+ Triggered when a record is updated locally. The coordinator pulls the complete merged record from the database to support standard REST `PUT` schemas:
118
+ ```mermaid
119
+ sequenceDiagram
120
+ participant Store as data-ln-data-store
121
+ participant Coord as data-ln-data-coordinator
122
+ participant Gateway as data-ln-*-connector
123
+
124
+ Store->>Coord: Event: ln-store:request-remote-update (id, expected_version)
125
+ Coord->>Store: JS API: store.getById(id)
126
+ Store-->>Coord: Returns local record
127
+ Note over Coord: Applies Egress Mapper:<br/>mapper.egress(cleanRecord)
128
+ Coord->>Gateway: JS API: connector.update(id, serverPayload, expected_version)
129
+ alt Server Success
130
+ Gateway-->>Coord: Promise: returns serverRawResponse
131
+ Note over Coord: Applies Ingress Mapper:<br/>mapper.ingress(serverRaw)
132
+ Coord->>Store: JS API: store.confirmMutation(id, confirmedLocalRecord, 'update')
133
+ else Conflict (409)
134
+ Gateway-->>Coord: Promise: Rejects with 409
135
+ Note over Coord: Applies Ingress Mapper:<br/>mapper.ingress(remoteRecord)
136
+ Coord->>Store: JS API: store.resolveConflict(id, remoteRecord, fieldDiffs)
137
+ else Other Failure
138
+ Gateway-->>Coord: Promise: Rejects
139
+ Coord->>Store: JS API: store.revertMutation(id, 'update', errorMessage)
140
+ end
141
+ ```
142
+
143
+ ### 4. Deletions (`ln-store:request-remote-delete` & `bulk-delete`)
144
+ Triggered when records are deleted in-memory:
145
+ * **Single Delete**: Calls `connector.delete(id)` $\rightarrow$ calls `store.confirmMutation(id, null, 'delete')` (or `revertMutation` on failure).
146
+ * **Bulk Delete**: Calls `connector.bulkDelete(ids)` $\rightarrow$ calls `store.confirmMutation(ids.join(','), null, 'bulk-delete')` (or `revertMutation` on failure).
147
+
148
+ ---
149
+
150
+ ## 💡 JS API (On the element)
151
+
152
+ Access the coordinator instance programmatically via the `lnDataCoordinator` or `lnCoordinator` properties:
153
+
154
+ ```javascript
155
+ const coordinator = document.querySelector('[data-ln-data-coordinator="documents"]').lnDataCoordinator;
156
+
157
+ // Force a remote configuration refresh
158
+ coordinator.refreshConfig();
159
+
160
+ // Retrieve children objects
161
+ const { store, connector } = coordinator.findChildren();
162
+
163
+ // Fetch currently resolved mapper functions
164
+ const { ingress, egress } = coordinator.mapper;
165
+ ```
@@ -0,0 +1 @@
1
+ (function(){"use strict";function g(a,i){if(!document.body){document.addEventListener("DOMContentLoaded",function(){g(a,i)}),console.warn("["+i+'] Script loaded before <body> — add "defer" to your <script> tag');return}a()}function y(a,i,l,u){if(a.nodeType!==1)return;const f=i.indexOf("[")!==-1||i.indexOf(".")!==-1||i.indexOf("#")!==-1?i:"["+i+"]",e=Array.from(a.querySelectorAll(f));a.matches&&a.matches(f)&&e.push(a);for(const n of e)n[l]||(n[l]=new u(n))}function v(a,i,l,u,p={}){const f=p.extraAttributes||[],e=p.onAttributeChange||null,n=p.onInit||null;function t(d){const r=d||document.body;y(r,a,i,l),n&&n(r)}return g(function(){const d=new MutationObserver(function(o){for(let c=0;c<o.length;c++){const s=o[c];if(s.type==="childList")for(let m=0;m<s.addedNodes.length;m++){const h=s.addedNodes[m];h.nodeType===1&&(y(h,a,i,l),n&&n(h))}else s.type==="attributes"&&(e&&s.target[i]?e(s.target,s.attributeName):(y(s.target,a,i,l),n&&n(s.target)))}});let r=[];if(a.indexOf("[")!==-1){const o=/\[([\w-]+)/g;let c;for(;(c=o.exec(a))!==null;)r.push(c[1])}else r.push(a);d.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:r.concat(f)})},u),window[i]=t,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){t(document.body)}):t(document.body),t}const b={};function M(a,i){b[a]=i}function C(a){return b[a]||{ingress:i=>i,egress:i=>i}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=M,window.lnCore.getDataMapper=C),(function(){const a="data-ln-data-coordinator",i="lnDataCoordinator",l="lnCoordinator";if(window[i]!==void 0)return;function u(e){return this.dom=e,this._name=e.getAttribute(a),e[i]=this,e[l]=this,this.mapper=null,this._handlers=null,this.refreshMapper(),p(this),this}u.prototype.refreshMapper=function(){this.mapper=null,this.dom.querySelector("script[data-ln-mapper]")&&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.");const n=this.dom.getAttribute("data-ln-data-mapper")||this.dom.getAttribute("data-ln-data-coordinator");n&&window.lnCore&&typeof window.lnCore.getDataMapper=="function"&&(this.mapper=window.lnCore.getDataMapper(n)),this.mapper||(this.mapper={}),typeof this.mapper.ingress!="function"&&(this.mapper.ingress=function(t){return t}),typeof this.mapper.egress!="function"&&(this.mapper.egress=function(t){return t})},u.prototype.findChildren=function(){const e=this.dom.querySelector("[data-ln-data-store]"),n=this.dom.querySelector("[data-ln-api-connector], [data-ln-couchdb-connector], [data-ln-websocket-connector], [data-ln-rest-connector]");return{storeEl:e,connectorEl:n,store:e?e.lnDataStore||e.lnStore:null,connector:n?n.lnConnector||n.lnApiConnector||n.lnCouchDbConnector:null}};function p(e){e._handlers={sync:function(n){e.refreshMapper();const t=e.findChildren();if(!t.store||!t.connector){console.warn("[ln-data-coordinator] Cannot sync: store or connector not found in subtree");return}const d=n.detail.since;t.connector.fetchDelta(d).then(function(r){let o=[],c=[],s=null;r&&Array.isArray(r)?(o=r,s=Math.floor(Date.now()/1e3)):r&&(o=Array.isArray(r.data)?r.data:[],c=Array.isArray(r.deleted)?r.deleted:[],s=r.synced_at!==void 0?r.synced_at:r.since!==void 0?r.since:null);const m=o.map(h=>e.mapper.ingress(h));t.store.applySync(m,c,s)}).catch(function(r){console.error("[ln-data-coordinator] Sync failed:",r)})},create:function(n){e.refreshMapper();const t=e.findChildren();if(!t.store||!t.connector)return;const d=n.detail.tempId,r=n.detail.data||{},o=e.mapper.egress(r);t.connector.create(o).then(function(c){const s=e.mapper.ingress(c);t.store.confirmMutation(d,s,"create")}).catch(function(c){console.error("[ln-data-coordinator] Create mutation failed:",c),t.store.revertMutation(d,"create",c.message||c)})},update:function(n){e.refreshMapper();const t=e.findChildren();if(!t.store||!t.connector)return;const d=n.detail.id,r=n.detail.expected_version;t.store.getById(d).then(function(o){if(!o)throw new Error("Record not found in cache store: "+d);const c=Object.assign({},o);delete c._pending;const s=e.mapper.egress(c);return t.connector.update(d,s,r)}).then(function(o){const c=e.mapper.ingress(o);t.store.confirmMutation(d,c,"update")}).catch(function(o){if(console.error("[ln-data-coordinator] Update mutation failed:",o),o.status===409){const c=o.data&&o.data.remote?e.mapper.ingress(o.data.remote):null,s=o.data?o.data.field_diffs:null;t.store.resolveConflict(d,c,s)}else t.store.revertMutation(d,"update",o.message||o)})},delete:function(n){e.refreshMapper();const t=e.findChildren();if(!t.store||!t.connector)return;const d=n.detail.id;t.connector.delete(d).then(function(){t.store.confirmMutation(d,null,"delete")}).catch(function(r){console.error("[ln-data-coordinator] Delete mutation failed:",r),t.store.revertMutation(d,"delete",r.message||r)})},bulkDelete:function(n){e.refreshMapper();const t=e.findChildren();if(!t.store||!t.connector)return;const d=n.detail.ids||[],r=d.join(",");t.connector.bulkDelete(d).then(function(){t.store.confirmMutation(r,null,"bulk-delete")}).catch(function(o){console.error("[ln-data-coordinator] Bulk delete mutation failed:",o),t.store.revertMutation(r,"bulk-delete",o.message||o)})}},e.dom.addEventListener("ln-store:request-remote-sync",e._handlers.sync),e.dom.addEventListener("ln-store:request-remote-create",e._handlers.create),e.dom.addEventListener("ln-store:request-remote-update",e._handlers.update),e.dom.addEventListener("ln-store:request-remote-delete",e._handlers.delete),e.dom.addEventListener("ln-store:request-remote-bulk-delete",e._handlers.bulkDelete)}u.prototype.destroy=function(){if(!this.dom[i])return;const e=this;e._handlers&&(e.dom.removeEventListener("ln-store:request-remote-sync",e._handlers.sync),e.dom.removeEventListener("ln-store:request-remote-create",e._handlers.create),e.dom.removeEventListener("ln-store:request-remote-update",e._handlers.update),e.dom.removeEventListener("ln-store:request-remote-delete",e._handlers.delete),e.dom.removeEventListener("ln-store:request-remote-bulk-delete",e._handlers.bulkDelete),e._handlers=null),delete this.dom[i],delete this.dom[l]};function f(e,n){const t=e[i];t&&n==="data-ln-data-mapper"&&t.refreshMapper()}v(a,i,u,"ln-data-coordinator",{extraAttributes:["data-ln-data-mapper"],onAttributeChange:f})})()})();