@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,699 @@
1
+ import { registerComponent, dispatch, setCryptoKey, getCryptoKey, encryptData, decryptData } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-data-store';
5
+ const DOM_ATTRIBUTE = 'lnDataStore';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ const DB_NAME = 'ln_app_cache';
10
+ const META_STORE = '_meta';
11
+ const SCHEMA_VERSION = '1.0';
12
+
13
+ let _db = null;
14
+ let _dbReady = null;
15
+ const _stores = {};
16
+
17
+ function _uuid() {
18
+ try { return crypto.randomUUID(); }
19
+ catch (_) {
20
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
21
+ const r = Math.random() * 16 | 0;
22
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
23
+ });
24
+ }
25
+ }
26
+
27
+ function _checkQuota(err) {
28
+ if (err && err.name === 'QuotaExceededError') {
29
+ dispatch(document, 'ln-store:quota-exceeded', { error: err });
30
+ }
31
+ }
32
+
33
+ // ─── Database ──────────────────────────────────────────
34
+
35
+ function _getRequiredStores() {
36
+ const required = {};
37
+ for (const el of document.querySelectorAll(`[${DOM_SELECTOR}]`)) {
38
+ const name = el.getAttribute(DOM_SELECTOR);
39
+ if (name) {
40
+ const indexAttr = el.getAttribute('data-ln-data-store-indexes') || el.getAttribute('data-ln-store-indexes') || '';
41
+ required[name] = {
42
+ indexes: indexAttr.split(',').map(s => s.trim()).filter(Boolean)
43
+ };
44
+ }
45
+ }
46
+ return required;
47
+ }
48
+
49
+ function _openDatabase() {
50
+ if (_dbReady) return _dbReady;
51
+
52
+ _dbReady = new Promise(resolve => {
53
+ if (typeof indexedDB === 'undefined') {
54
+ console.warn('[ln-data-store] IndexedDB not available — falling back to in-memory store');
55
+ return resolve(null);
56
+ }
57
+
58
+ const required = _getRequiredStores();
59
+ const requiredNames = Object.keys(required);
60
+ const probe = indexedDB.open(DB_NAME);
61
+
62
+ probe.onerror = () => {
63
+ console.warn('[ln-data-store] IndexedDB open failed — falling back to in-memory store');
64
+ resolve(null);
65
+ };
66
+
67
+ probe.onsuccess = e => {
68
+ const db = e.target.result;
69
+ const existing = Array.from(db.objectStoreNames);
70
+ const needsUpgrade = !existing.includes(META_STORE) || requiredNames.some(name => !existing.includes(name));
71
+
72
+ if (!needsUpgrade) {
73
+ _setupVersionChangeHandler(db);
74
+ _db = db;
75
+ return resolve(db);
76
+ }
77
+
78
+ const currentVersion = db.version;
79
+ db.close();
80
+
81
+ const upgrade = indexedDB.open(DB_NAME, currentVersion + 1);
82
+
83
+ upgrade.onblocked = () => {
84
+ console.warn('[ln-data-store] Database upgrade blocked — waiting for other tabs to close connection');
85
+ };
86
+
87
+ upgrade.onerror = () => {
88
+ console.warn('[ln-data-store] Database upgrade failed');
89
+ resolve(null);
90
+ };
91
+
92
+ upgrade.onupgradeneeded = e => {
93
+ const db = e.target.result;
94
+ if (!db.objectStoreNames.contains(META_STORE)) {
95
+ db.createObjectStore(META_STORE, { keyPath: 'key' });
96
+ }
97
+ for (const storeName of requiredNames) {
98
+ if (!db.objectStoreNames.contains(storeName)) {
99
+ const store = db.createObjectStore(storeName, { keyPath: 'id' });
100
+ for (const idx of required[storeName].indexes) {
101
+ store.createIndex(idx, idx, { unique: false });
102
+ }
103
+ }
104
+ }
105
+ };
106
+
107
+ upgrade.onsuccess = e => {
108
+ const db = e.target.result;
109
+ _setupVersionChangeHandler(db);
110
+ _db = db;
111
+ resolve(db);
112
+ };
113
+ };
114
+ });
115
+
116
+ return _dbReady;
117
+ }
118
+
119
+ function _setupVersionChangeHandler(db) {
120
+ db.onversionchange = () => {
121
+ db.close();
122
+ _db = null;
123
+ _dbReady = null;
124
+ };
125
+ }
126
+
127
+ function _getDb() {
128
+ if (_db) return Promise.resolve(_db);
129
+ _dbReady = null;
130
+ return _openDatabase();
131
+ }
132
+
133
+ // ─── Cryptographic Wrappers (DRY using ln-core) ──────────
134
+
135
+ async function _encryptRecord(record) {
136
+ if (!getCryptoKey() || !record) return record;
137
+
138
+ // Isolate ID and metadata we want in plain text for IndexedDB queries
139
+ const plainRecord = { ...record };
140
+ const recordId = plainRecord.id;
141
+ const pending = plainRecord._pending;
142
+
143
+ // Encrypt payload using core helper
144
+ const encryptedPayload = await encryptData(plainRecord);
145
+ if (!encryptedPayload || !encryptedPayload.encrypted) return record;
146
+
147
+ return {
148
+ id: recordId,
149
+ _pending: pending,
150
+ encrypted: true,
151
+ iv: encryptedPayload.iv,
152
+ data: encryptedPayload.data
153
+ };
154
+ }
155
+
156
+ async function _decryptRecord(record) {
157
+ if (!record || !record.encrypted || !getCryptoKey()) return record;
158
+ return decryptData(record);
159
+ }
160
+
161
+ // ─── IndexedDB CRUD Helpers ────────────────────────────
162
+
163
+ const _tx = (storeName, mode) => _getDb().then(db => db ? db.transaction(storeName, mode).objectStore(storeName) : null);
164
+
165
+ function _idbRequest(request) {
166
+ return new Promise((resolve, reject) => {
167
+ request.onsuccess = () => resolve(request.result);
168
+ request.onerror = () => {
169
+ _checkQuota(request.error);
170
+ reject(request.error);
171
+ };
172
+ });
173
+ }
174
+
175
+ const _getAllRecords = storeName => _tx(storeName, 'readonly')
176
+ .then(store => store ? _idbRequest(store.getAll()) : [])
177
+ .then(records => getCryptoKey() ? Promise.all(records.map(r => _decryptRecord(r))) : records);
178
+
179
+ const _getRecord = (storeName, id) => _tx(storeName, 'readonly')
180
+ .then(store => store ? _idbRequest(store.get(id)) : null)
181
+ .then(record => record ? _decryptRecord(record) : null);
182
+
183
+ const _putRecord = (storeName, record) => {
184
+ const prepPromise = getCryptoKey() ? _encryptRecord(record) : Promise.resolve(record);
185
+ return prepPromise.then(prepped => _tx(storeName, 'readwrite').then(store => store ? _idbRequest(store.put(prepped)) : null));
186
+ };
187
+
188
+ const _deleteRecord = (storeName, id) => _tx(storeName, 'readwrite').then(store => store ? _idbRequest(store.delete(id)) : null);
189
+ const _clearStore = storeName => _tx(storeName, 'readwrite').then(store => store ? _idbRequest(store.clear()) : null);
190
+ const _countRecords = storeName => _tx(storeName, 'readonly').then(store => store ? _idbRequest(store.count()) : 0);
191
+
192
+ // ─── Meta Store ────────────────────────────────────────
193
+
194
+ const _getMeta = storeName => _tx(META_STORE, 'readonly').then(store => store ? _idbRequest(store.get(storeName)) : null);
195
+ const _setMeta = (storeName, data) => _tx(META_STORE, 'readwrite').then(store => {
196
+ if (!store) return;
197
+ data.key = storeName;
198
+ return _idbRequest(store.put(data));
199
+ });
200
+
201
+ // ─── Component Constructor ─────────────────────────────
202
+
203
+ function _component(dom) {
204
+ this.dom = dom;
205
+ this._name = dom.getAttribute(DOM_SELECTOR);
206
+
207
+ const staleAttr = dom.getAttribute('data-ln-data-store-stale') || dom.getAttribute('data-ln-store-stale');
208
+ const _parsed = parseInt(staleAttr, 10);
209
+ this._staleThreshold = (staleAttr === 'never' || staleAttr === '-1') ? -1 : (isNaN(_parsed) ? 300 : _parsed);
210
+
211
+ const searchAttr = dom.getAttribute('data-ln-data-store-search-fields') || dom.getAttribute('data-ln-store-search-fields') || '';
212
+ this._searchFields = searchAttr.split(',').map(s => s.trim()).filter(Boolean);
213
+
214
+ this._handlers = null;
215
+ this._pendingSnapshots = {};
216
+
217
+ this.isLoaded = false;
218
+ this.isSyncing = false;
219
+ this.lastSyncedAt = null;
220
+ this.totalCount = 0;
221
+ this.presenters = null;
222
+
223
+ _stores[this._name] = this;
224
+
225
+ _bindEvents(this);
226
+ _initStore(this);
227
+ return this;
228
+ }
229
+
230
+ // ─── DOM Mutation Requests Listeners ────────────────────
231
+
232
+ function _bindEvents(self) {
233
+ self._handlers = {
234
+ 'create': e => _handleCreateRequest(self, e.detail),
235
+ 'update': e => _handleUpdateRequest(self, e.detail),
236
+ 'delete': e => _handleDeleteRequest(self, e.detail),
237
+ 'bulk-delete': e => _handleBulkDeleteRequest(self, e.detail)
238
+ };
239
+ for (const [event, fn] of Object.entries(self._handlers)) {
240
+ self.dom.addEventListener(`ln-store:request-${event}`, fn);
241
+ }
242
+ }
243
+
244
+ // ─── Optimistic Writing Pipeline ─────────────────────────
245
+
246
+ function _handleCreateRequest(self, { data = {} } = {}) {
247
+ const tempId = `_temp_${_uuid()}`;
248
+ const record = { ...data, id: tempId, _pending: true };
249
+
250
+ _putRecord(self._name, record).then(() => {
251
+ self.totalCount++;
252
+ dispatch(self.dom, 'ln-store:created', { store: self._name, record, tempId });
253
+ dispatch(self.dom, 'ln-store:request-remote-create', { tempId, data });
254
+ });
255
+ }
256
+
257
+ function _handleUpdateRequest(self, { id, data = {}, expected_version } = {}) {
258
+ _getRecord(self._name, id).then(existing => {
259
+ if (!existing) throw new Error(`Record not found: ${id}`);
260
+
261
+ self._pendingSnapshots[id] = { ...existing };
262
+ const updated = { ...existing, ...data, _pending: true };
263
+
264
+ return _putRecord(self._name, updated).then(() => {
265
+ dispatch(self.dom, 'ln-store:updated', { store: self._name, record: updated, previous: self._pendingSnapshots[id] });
266
+ dispatch(self.dom, 'ln-store:request-remote-update', { id, data, expected_version });
267
+ });
268
+ }).catch(err => console.error('[ln-data-store] Optimistic update failed:', err));
269
+ }
270
+
271
+ function _handleDeleteRequest(self, { id } = {}) {
272
+ _getRecord(self._name, id).then(existing => {
273
+ if (!existing) return;
274
+
275
+ self._pendingSnapshots[id] = { ...existing };
276
+
277
+ return _deleteRecord(self._name, id).then(() => {
278
+ self.totalCount--;
279
+ dispatch(self.dom, 'ln-store:deleted', { store: self._name, id });
280
+ dispatch(self.dom, 'ln-store:request-remote-delete', { id });
281
+ });
282
+ }).catch(err => console.error('[ln-data-store] Optimistic delete failed:', err));
283
+ }
284
+
285
+ function _handleBulkDeleteRequest(self, { ids = [] } = {}) {
286
+ if (!ids.length) return;
287
+
288
+ Promise.all(ids.map(id => _getRecord(self._name, id))).then(records => {
289
+ const savedRecords = records.filter(Boolean);
290
+ const savedIds = savedRecords.map(r => r.id);
291
+
292
+ self._pendingSnapshots[savedIds.join(',')] = savedRecords;
293
+
294
+ return _deleteBulk(self._name, savedIds).then(() => {
295
+ self.totalCount -= savedIds.length;
296
+ dispatch(self.dom, 'ln-store:deleted', { store: self._name, ids: savedIds });
297
+ dispatch(self.dom, 'ln-store:request-remote-bulk-delete', { ids: savedIds });
298
+ });
299
+ }).catch(err => console.error('[ln-data-store] Optimistic bulk delete failed:', err));
300
+ }
301
+
302
+ // ─── Initialization ────────────────────────────────────
303
+
304
+ function _initStore(self) {
305
+ _openDatabase().then(() => _getMeta(self._name)).then(meta => {
306
+ if (meta && meta.schema_version === SCHEMA_VERSION) {
307
+ self.lastSyncedAt = meta.last_synced_at || null;
308
+ self.totalCount = meta.record_count || 0;
309
+
310
+ if (self.totalCount > 0) {
311
+ self.isLoaded = true;
312
+ dispatch(self.dom, 'ln-store:ready', { store: self._name, count: self.totalCount, source: 'cache' });
313
+ if (_isStale(self)) _triggerRemoteSync(self);
314
+ } else {
315
+ _triggerRemoteSync(self);
316
+ }
317
+ } else if (meta && meta.schema_version !== SCHEMA_VERSION) {
318
+ _clearStore(self._name)
319
+ .then(() => _setMeta(self._name, { schema_version: SCHEMA_VERSION, last_synced_at: null, record_count: 0 }))
320
+ .then(() => _triggerRemoteSync(self));
321
+ } else {
322
+ _triggerRemoteSync(self);
323
+ }
324
+ });
325
+ }
326
+
327
+ function _isStale(self) {
328
+ if (self._staleThreshold === -1) return false;
329
+ if (!self.lastSyncedAt) return true;
330
+ return (Math.floor(Date.now() / 1000) - self.lastSyncedAt) > self._staleThreshold;
331
+ }
332
+
333
+ function _triggerRemoteSync(self) {
334
+ self.isSyncing = true;
335
+ dispatch(self.dom, 'ln-store:request-remote-sync', { since: self.lastSyncedAt });
336
+ }
337
+
338
+ // ─── Bulk IndexedDB Operations ─────────────────────────
339
+
340
+ function _putBulk(storeName, records) {
341
+ return _getDb().then(db => {
342
+ if (!db) return;
343
+
344
+ const prepPromise = getCryptoKey()
345
+ ? Promise.all(records.map(r => _encryptRecord(r)))
346
+ : Promise.resolve(records);
347
+
348
+ return prepPromise.then(preppedRecords => {
349
+ return new Promise((resolve, reject) => {
350
+ const tx = db.transaction(storeName, 'readwrite');
351
+ const store = tx.objectStore(storeName);
352
+ preppedRecords.forEach(r => store.put(r));
353
+ tx.oncomplete = () => resolve();
354
+ tx.onerror = () => {
355
+ _checkQuota(tx.error);
356
+ reject(tx.error);
357
+ };
358
+ });
359
+ });
360
+ });
361
+ }
362
+
363
+ function _deleteBulk(storeName, ids) {
364
+ return _getDb().then(db => {
365
+ if (!db) return;
366
+ return new Promise((resolve, reject) => {
367
+ const tx = db.transaction(storeName, 'readwrite');
368
+ const store = tx.objectStore(storeName);
369
+ ids.forEach(id => store.delete(id));
370
+ tx.oncomplete = () => resolve();
371
+ tx.onerror = () => reject(tx.error);
372
+ });
373
+ });
374
+ }
375
+
376
+ // ─── Visibility Auto-Sync check ───
377
+
378
+ let _visibilityHandler = () => {
379
+ if (document.visibilityState !== 'visible') return;
380
+ Object.values(_stores).forEach(inst => {
381
+ if (inst.isLoaded && !inst.isSyncing && _isStale(inst)) {
382
+ _triggerRemoteSync(inst);
383
+ }
384
+ });
385
+ };
386
+ document.addEventListener('visibilitychange', _visibilityHandler);
387
+
388
+ // ─── Query Engine (In-Memory over Cache) ───────────────
389
+
390
+ const _collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
391
+
392
+ function _sort(records, sort) {
393
+ if (!sort || !sort.field) return records;
394
+ const { field, direction } = sort;
395
+ const desc = direction === 'desc';
396
+
397
+ return [...records].sort((a, b) => {
398
+ const va = a[field];
399
+ const vb = b[field];
400
+
401
+ if (va == null && vb == null) return 0;
402
+ if (va == null) return desc ? 1 : -1;
403
+ if (vb == null) return desc ? -1 : 1;
404
+
405
+ const result = (typeof va === 'string' && typeof vb === 'string')
406
+ ? _collator.compare(va, vb)
407
+ : (va < vb ? -1 : va > vb ? 1 : 0);
408
+
409
+ return desc ? -result : result;
410
+ });
411
+ }
412
+
413
+ function _filter(records, filters) {
414
+ if (!filters) return records;
415
+ const keys = Object.keys(filters).filter(k => Array.isArray(filters[k]) && filters[k].length > 0);
416
+ if (!keys.length) return records;
417
+
418
+ return records.filter(record =>
419
+ keys.every(field => filters[field].map(String).includes(String(record[field])))
420
+ );
421
+ }
422
+
423
+ function _search(records, query, searchFields) {
424
+ if (!query || !searchFields || !searchFields.length) return records;
425
+ const lower = query.toLowerCase();
426
+ return records.filter(record =>
427
+ searchFields.some(field => {
428
+ const val = record[field];
429
+ return val != null && String(val).toLowerCase().includes(lower);
430
+ })
431
+ );
432
+ }
433
+
434
+ function _aggregate(records, field, fn) {
435
+ if (!records.length) return 0;
436
+ if (fn === 'count') return records.length;
437
+
438
+ const numbers = records.map(r => parseFloat(r[field])).filter(v => !isNaN(v));
439
+ const sum = numbers.reduce((a, b) => a + b, 0);
440
+
441
+ if (fn === 'sum') return sum;
442
+ if (fn === 'avg') return numbers.length ? sum / numbers.length : 0;
443
+ return 0;
444
+ }
445
+
446
+ function _decorate(self, records) {
447
+ if (!self.presenters || !self.presenters.computed) return records;
448
+ const computed = self.presenters.computed;
449
+ return records.map(record => {
450
+ const copy = { ...record };
451
+ for (const [fieldName, fn] of Object.entries(computed)) {
452
+ try {
453
+ copy[fieldName] = fn(record);
454
+ } catch (err) {
455
+ console.error(`[ln-data-store] Decorator computed field failed for ${fieldName}`, err);
456
+ }
457
+ }
458
+ return copy;
459
+ });
460
+ }
461
+
462
+ // ─── Public CRUD & Sync APIs ───────────────────────────
463
+
464
+ _component.prototype.getAll = function (options = {}) {
465
+ const self = this;
466
+ return _getAllRecords(self._name).then(records => {
467
+ const total = records.length;
468
+
469
+ if (options.filters) records = _filter(records, options.filters);
470
+ if (options.search) records = _search(records, options.search, self._searchFields);
471
+
472
+ const filtered = records.length;
473
+
474
+ if (options.sort) records = _sort(records, options.sort);
475
+
476
+ if (options.offset || options.limit) {
477
+ const offset = options.offset || 0;
478
+ const limit = options.limit || records.length;
479
+ records = records.slice(offset, offset + limit);
480
+ }
481
+
482
+ return {
483
+ data: _decorate(self, records),
484
+ total,
485
+ filtered
486
+ };
487
+ });
488
+ };
489
+
490
+ _component.prototype.getById = function (id) {
491
+ return _getRecord(this._name, id).then(record => record ? _decorate(this, [record])[0] : null);
492
+ };
493
+
494
+ _component.prototype.count = function (filters) {
495
+ return filters
496
+ ? _getAllRecords(this._name).then(records => _filter(records, filters).length)
497
+ : _countRecords(this._name);
498
+ };
499
+
500
+ _component.prototype.aggregate = function (field, fn) {
501
+ return _getAllRecords(this._name).then(records => _aggregate(records, field, fn));
502
+ };
503
+
504
+ _component.prototype.setPresenters = function (presenters) {
505
+ this.presenters = presenters;
506
+ };
507
+
508
+ // ─── Public Remote Response Synchronization Methods ────
509
+
510
+ _component.prototype.applySync = function (upsertedRecords, deletedIds, syncedAt) {
511
+ const self = this;
512
+ const hasChanges = upsertedRecords.length > 0 || deletedIds.length > 0;
513
+
514
+ let chain = Promise.resolve();
515
+ if (upsertedRecords.length > 0) chain = chain.then(() => _putBulk(self._name, upsertedRecords));
516
+ if (deletedIds.length > 0) chain = chain.then(() => _deleteBulk(self._name, deletedIds));
517
+
518
+ return chain.then(() => _countRecords(self._name)).then(count => {
519
+ self.totalCount = count;
520
+ return _setMeta(self._name, {
521
+ schema_version: SCHEMA_VERSION,
522
+ last_synced_at: syncedAt,
523
+ record_count: count
524
+ });
525
+ }).then(() => {
526
+ const isInitialLoad = !self.isLoaded;
527
+ self.isLoaded = true;
528
+ self.isSyncing = false;
529
+ self.lastSyncedAt = syncedAt;
530
+
531
+ if (isInitialLoad) {
532
+ dispatch(self.dom, 'ln-store:loaded', { store: self._name, count: self.totalCount });
533
+ dispatch(self.dom, 'ln-store:ready', { store: self._name, count: self.totalCount, source: 'server' });
534
+ } else {
535
+ dispatch(self.dom, 'ln-store:synced', {
536
+ store: self._name,
537
+ added: upsertedRecords.length,
538
+ deleted: deletedIds.length,
539
+ changed: hasChanges
540
+ });
541
+ }
542
+ }).catch(err => {
543
+ self.isSyncing = false;
544
+ console.error('[ln-data-store] applySync failed:', err);
545
+ });
546
+ };
547
+
548
+ _component.prototype.confirmMutation = function (tempIdOrId, serverRecord, action) {
549
+ const self = this;
550
+ const handlers = {
551
+ create: () => _deleteRecord(self._name, tempIdOrId)
552
+ .then(() => _putRecord(self._name, serverRecord))
553
+ .then(() => {
554
+ delete self._pendingSnapshots[tempIdOrId];
555
+ dispatch(self.dom, 'ln-store:confirmed', { store: self._name, record: serverRecord, tempId: tempIdOrId, action: 'create' });
556
+ }),
557
+ update: () => _putRecord(self._name, serverRecord).then(() => {
558
+ delete self._pendingSnapshots[tempIdOrId];
559
+ dispatch(self.dom, 'ln-store:confirmed', { store: self._name, record: serverRecord, action: 'update' });
560
+ }),
561
+ delete: () => {
562
+ delete self._pendingSnapshots[tempIdOrId];
563
+ dispatch(self.dom, 'ln-store:confirmed', { store: self._name, record: null, action: 'delete' });
564
+ return Promise.resolve();
565
+ },
566
+ 'bulk-delete': () => {
567
+ delete self._pendingSnapshots[tempIdOrId];
568
+ dispatch(self.dom, 'ln-store:confirmed', { store: self._name, record: null, ids: tempIdOrId.split(','), action: 'bulk-delete' });
569
+ return Promise.resolve();
570
+ }
571
+ };
572
+ return handlers[action] ? handlers[action]() : Promise.resolve();
573
+ };
574
+
575
+ _component.prototype.revertMutation = function (tempIdOrId, action, error) {
576
+ const self = this;
577
+ const fallbackErr = error || `Server rejected ${action}`;
578
+
579
+ const handlers = {
580
+ create: () => _deleteRecord(self._name, tempIdOrId).then(() => {
581
+ self.totalCount--;
582
+ delete self._pendingSnapshots[tempIdOrId];
583
+ dispatch(self.dom, 'ln-store:reverted', { store: self._name, record: null, action: 'create', error: fallbackErr });
584
+ }),
585
+ update: () => {
586
+ const previous = self._pendingSnapshots[tempIdOrId];
587
+ if (!previous) return Promise.resolve();
588
+ return _putRecord(self._name, previous).then(() => {
589
+ delete self._pendingSnapshots[tempIdOrId];
590
+ dispatch(self.dom, 'ln-store:reverted', { store: self._name, record: previous, action: 'update', error: fallbackErr });
591
+ });
592
+ },
593
+ delete: () => {
594
+ const saved = self._pendingSnapshots[tempIdOrId];
595
+ if (!saved) return Promise.resolve();
596
+ return _putRecord(self._name, saved).then(() => {
597
+ self.totalCount++;
598
+ delete self._pendingSnapshots[tempIdOrId];
599
+ dispatch(self.dom, 'ln-store:reverted', { store: self._name, record: saved, action: 'delete', error: fallbackErr });
600
+ });
601
+ },
602
+ 'bulk-delete': () => {
603
+ const savedRecords = self._pendingSnapshots[tempIdOrId];
604
+ if (!savedRecords || !savedRecords.length) return Promise.resolve();
605
+ return _putBulk(self._name, savedRecords).then(() => {
606
+ self.totalCount += savedRecords.length;
607
+ delete self._pendingSnapshots[tempIdOrId];
608
+ dispatch(self.dom, 'ln-store:reverted', { store: self._name, record: null, ids: tempIdOrId.split(','), action: 'bulk-delete', error: fallbackErr });
609
+ });
610
+ }
611
+ };
612
+ return handlers[action] ? handlers[action]() : Promise.resolve();
613
+ };
614
+
615
+ _component.prototype.resolveConflict = function (id, remoteRecord, fieldDiffs) {
616
+ const previous = this._pendingSnapshots[id];
617
+ if (!previous) return Promise.resolve();
618
+
619
+ return _putRecord(this._name, previous).then(() => {
620
+ delete this._pendingSnapshots[id];
621
+ dispatch(this.dom, 'ln-store:conflict', {
622
+ store: this._name,
623
+ local: previous,
624
+ remote: remoteRecord,
625
+ field_diffs: fieldDiffs || null
626
+ });
627
+ });
628
+ };
629
+
630
+ // ─── Manual Triggers & Cleanup ─────────────────────────
631
+
632
+ _component.prototype.forceSync = function () {
633
+ _triggerRemoteSync(this);
634
+ };
635
+
636
+ _component.prototype.fullReload = function () {
637
+ const self = this;
638
+ return _clearStore(self._name).then(() => {
639
+ self.isLoaded = false;
640
+ self.lastSyncedAt = null;
641
+ self.totalCount = 0;
642
+ _triggerRemoteSync(self);
643
+ });
644
+ };
645
+
646
+ _component.prototype.destroy = function () {
647
+ if (this._handlers) {
648
+ for (const [event, fn] of Object.entries(this._handlers)) {
649
+ this.dom.removeEventListener(`ln-store:request-${event}`, fn);
650
+ }
651
+ this._handlers = null;
652
+ }
653
+
654
+ delete _stores[this._name];
655
+
656
+ if (Object.keys(_stores).length === 0 && _visibilityHandler) {
657
+ document.removeEventListener('visibilitychange', _visibilityHandler);
658
+ _visibilityHandler = null;
659
+ }
660
+
661
+ delete this.dom[DOM_ATTRIBUTE];
662
+ dispatch(this.dom, 'ln-store:destroyed', { store: this._name });
663
+ };
664
+
665
+ // ─── clearAll (global) ─────────────────────────────────
666
+
667
+ function _clearAll() {
668
+ return _getDb().then(db => {
669
+ if (!db) return;
670
+ const storeNames = Array.from(db.objectStoreNames);
671
+ return new Promise((resolve, reject) => {
672
+ const tx = db.transaction(storeNames, 'readwrite');
673
+ storeNames.forEach(name => tx.objectStore(name).clear());
674
+ tx.oncomplete = () => resolve();
675
+ tx.onerror = () => reject(tx.error);
676
+ });
677
+ }).then(() => {
678
+ Object.values(_stores).forEach(inst => {
679
+ inst.isLoaded = false;
680
+ inst.isSyncing = false;
681
+ inst.lastSyncedAt = null;
682
+ inst.totalCount = 0;
683
+ });
684
+ });
685
+ }
686
+
687
+ // ─── Registration ──────────────────────────────────────
688
+
689
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-data-store');
690
+
691
+ window[DOM_ATTRIBUTE].clearAll = _clearAll;
692
+ window[DOM_ATTRIBUTE].init = window[DOM_ATTRIBUTE];
693
+ window[DOM_ATTRIBUTE].setStorageKey = setCryptoKey;
694
+
695
+ if (typeof window !== 'undefined') {
696
+ window.lnCore = window.lnCore || {};
697
+ window.lnCore.setStorageKey = setCryptoKey;
698
+ }
699
+ })();