@livenetworks/ashlar 2.0.6 → 2.0.8

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 (221) hide show
  1. package/LICENSE +641 -641
  2. package/components/COMPONENTS.md +1116 -1116
  3. package/components/index.js +81 -81
  4. package/components/ln-accordion/ln-accordion-dev.scss +8 -8
  5. package/components/ln-accordion/ln-accordion.js +1 -1
  6. package/components/ln-accordion/ln-accordion.schema.json +30 -30
  7. package/components/ln-accordion/src/ln-accordion.js +1 -0
  8. package/components/ln-ajax/ln-ajax-dev.scss +8 -8
  9. package/components/ln-ajax/ln-ajax.js +1 -1
  10. package/components/ln-ajax/ln-ajax.schema.json +30 -30
  11. package/components/ln-api-connector/ln-api-connector.js +1 -1
  12. package/components/ln-api-connector/ln-api-connector.schema.json +96 -96
  13. package/components/ln-api-connector/src/ln-api-connector.js +1 -0
  14. package/components/ln-api-queue/ln-api-queue.js +1 -1
  15. package/components/ln-api-queue/ln-api-queue.schema.json +34 -34
  16. package/components/ln-api-queue/src/ln-api-queue.js +3 -13
  17. package/components/ln-autoresize/ln-autoresize-dev.scss +8 -8
  18. package/components/ln-autoresize/ln-autoresize.js +1 -1
  19. package/components/ln-autoresize/ln-autoresize.schema.json +22 -22
  20. package/components/ln-autosave/ln-autosave-dev.scss +13 -13
  21. package/components/ln-autosave/ln-autosave.js +1 -1
  22. package/components/ln-autosave/ln-autosave.schema.json +48 -48
  23. package/components/ln-autosave/src/autosave-model.js +34 -34
  24. package/components/ln-chart/ln-chart.js +1 -1
  25. package/components/ln-chart/ln-chart.schema.json +146 -146
  26. package/components/ln-chart/src/chart-model.js +154 -154
  27. package/components/ln-circular-progress/ln-circular-progress.js +1 -1
  28. package/components/ln-circular-progress/ln-circular-progress.schema.json +42 -42
  29. package/components/ln-confirm/ln-confirm-dev.scss +24 -24
  30. package/components/ln-confirm/ln-confirm.js +1 -1
  31. package/components/ln-confirm/ln-confirm.schema.json +66 -66
  32. package/components/ln-confirm/src/ln-confirm.js +178 -178
  33. package/components/ln-core/crypto.js +286 -265
  34. package/components/ln-core/dom.js +115 -115
  35. package/components/ln-core/form.js +136 -136
  36. package/components/ln-core/helpers.js +33 -33
  37. package/components/ln-core/http.js +48 -48
  38. package/components/ln-core/index.js +1 -1
  39. package/components/ln-core/ln-core.schema.json +109 -109
  40. package/components/ln-core/locale.js +55 -55
  41. package/components/ln-core/matching.js +78 -78
  42. package/components/ln-core/number.js +125 -125
  43. package/components/ln-core/template.js +206 -206
  44. package/components/ln-couchdb-connector/README.md +4 -0
  45. package/components/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  46. package/components/ln-couchdb-connector/ln-couchdb-connector.schema.json +50 -50
  47. package/components/ln-couchdb-connector/src/ln-couchdb-connector.js +19 -1
  48. package/components/ln-data-coordinator/README.md +9 -38
  49. package/components/ln-data-coordinator/ln-data-coordinator-dev.scss +8 -8
  50. package/components/ln-data-coordinator/ln-data-coordinator.js +1 -1
  51. package/components/ln-data-coordinator/ln-data-coordinator.schema.json +16 -16
  52. package/components/ln-data-coordinator/src/data-read-policy.js +0 -7
  53. package/components/ln-data-coordinator/src/ln-data-coordinator.js +428 -1299
  54. package/components/ln-data-store/README.md +3 -2
  55. package/components/ln-data-store/ln-data-store-dev.scss +13 -13
  56. package/components/ln-data-store/ln-data-store.js +1 -1
  57. package/components/ln-data-store/ln-data-store.schema.json +84 -84
  58. package/components/ln-data-store/src/data-store-model.js +184 -155
  59. package/components/ln-data-store/src/ln-data-store.js +58 -11
  60. package/components/ln-date/README.md +4 -0
  61. package/components/ln-date/ln-date.js +1 -1
  62. package/components/ln-date/ln-date.schema.json +92 -92
  63. package/components/ln-date/src/date-model.js +186 -186
  64. package/components/ln-date/src/ln-date.js +3 -3
  65. package/components/ln-debug/ln-debug.js +1 -1
  66. package/components/ln-debug/ln-debug.schema.json +28 -28
  67. package/components/ln-debug/src/console-sink.js +19 -19
  68. package/components/ln-debug/src/gate.js +95 -95
  69. package/components/ln-debug/src/generated-attributes.js +2 -2
  70. package/components/ln-dropdown/ln-dropdown-dev.scss +13 -13
  71. package/components/ln-dropdown/ln-dropdown.js +1 -1
  72. package/components/ln-dropdown/ln-dropdown.schema.json +90 -90
  73. package/components/ln-editor/ln-editor.js +1 -1
  74. package/components/ln-editor/ln-editor.schema.json +58 -58
  75. package/components/ln-external-links/ln-external-links.js +1 -1
  76. package/components/ln-external-links/ln-external-links.schema.json +18 -18
  77. package/components/ln-fill/ln-fill.js +1 -1
  78. package/components/ln-fill/ln-fill.schema.json +23 -23
  79. package/components/ln-filter/ln-filter.js +1 -1
  80. package/components/ln-filter/ln-filter.schema.json +153 -153
  81. package/components/ln-filter/src/filter-model.js +185 -185
  82. package/components/ln-form/ln-form-dev.scss +8 -8
  83. package/components/ln-form/ln-form.js +1 -1
  84. package/components/ln-form/ln-form.schema.json +45 -45
  85. package/components/ln-http/ln-http.js +1 -1
  86. package/components/ln-http/ln-http.schema.json +6 -6
  87. package/components/ln-icon/ln-icon.schema.json +6 -6
  88. package/components/ln-include/ln-include.js +1 -1
  89. package/components/ln-include/ln-include.schema.json +23 -23
  90. package/components/ln-key/ln-key-dev.scss +18 -18
  91. package/components/ln-key/ln-key.js +1 -1
  92. package/components/ln-key/ln-key.schema.json +56 -56
  93. package/components/ln-key/src/ln-key.js +10 -10
  94. package/components/ln-link/ln-link-dev.scss +8 -8
  95. package/components/ln-link/ln-link.js +1 -1
  96. package/components/ln-link/ln-link.schema.json +22 -22
  97. package/components/ln-list/README.md +152 -152
  98. package/components/ln-list/ln-list-dev.scss +8 -8
  99. package/components/ln-list/ln-list.js +1 -1
  100. package/components/ln-list/ln-list.schema.json +166 -166
  101. package/components/ln-list/src/ln-list.js +4 -4
  102. package/components/ln-modal/README.md +4 -6
  103. package/components/ln-modal/ln-modal.js +1 -1
  104. package/components/ln-modal/ln-modal.schema.json +65 -65
  105. package/components/ln-modal/src/ln-modal.js +28 -8
  106. package/components/ln-nav/ln-nav.js +1 -1
  107. package/components/ln-nav/ln-nav.schema.json +25 -25
  108. package/components/ln-number/ln-number.js +1 -1
  109. package/components/ln-number/ln-number.schema.json +63 -63
  110. package/components/ln-number/src/number-model.js +31 -31
  111. package/components/ln-obfuscator/README.md +60 -60
  112. package/components/ln-obfuscator/ln-obfuscator.js +1 -1
  113. package/components/ln-obfuscator/ln-obfuscator.schema.json +49 -49
  114. package/components/ln-obfuscator/src/ln-obfuscator.js +159 -159
  115. package/components/ln-obfuscator/src/obfuscator-model.js +250 -250
  116. package/components/ln-options/ln-options.js +1 -1
  117. package/components/ln-options/ln-options.schema.json +41 -41
  118. package/components/ln-options/src/ln-options.js +1 -0
  119. package/components/ln-persist/README.md +151 -151
  120. package/components/ln-persist/ln-persist.js +1 -1
  121. package/components/ln-persist/ln-persist.schema.json +28 -28
  122. package/components/ln-persist/src/ln-persist.js +83 -83
  123. package/components/ln-picklist/README.md +230 -230
  124. package/components/ln-picklist/ln-picklist-dev.scss +22 -22
  125. package/components/ln-picklist/ln-picklist.js +1 -1
  126. package/components/ln-picklist/ln-picklist.schema.json +49 -49
  127. package/components/ln-picklist/src/ln-picklist.js +228 -228
  128. package/components/ln-popover/README.md +4 -0
  129. package/components/ln-popover/ln-popover-dev.scss +13 -13
  130. package/components/ln-popover/ln-popover.js +1 -1
  131. package/components/ln-popover/ln-popover.schema.json +82 -82
  132. package/components/ln-popover/src/ln-popover.js +9 -0
  133. package/components/ln-progress/ln-progress-dev.scss +8 -8
  134. package/components/ln-progress/ln-progress.js +1 -1
  135. package/components/ln-progress/ln-progress.schema.json +40 -40
  136. package/components/ln-router/ln-router.js +1 -1
  137. package/components/ln-router/ln-router.schema.json +76 -76
  138. package/components/ln-scroll/README.md +45 -45
  139. package/components/ln-scroll/ln-scroll.js +1 -1
  140. package/components/ln-scroll/ln-scroll.schema.json +85 -85
  141. package/components/ln-scroll/src/ln-scroll.js +150 -150
  142. package/components/ln-search/README.md +188 -188
  143. package/components/ln-search/ln-search.js +1 -1
  144. package/components/ln-search/ln-search.schema.json +103 -103
  145. package/components/ln-slug/ln-slug.js +1 -1
  146. package/components/ln-slug/ln-slug.schema.json +23 -23
  147. package/components/ln-sort/ln-sort.js +1 -1
  148. package/components/ln-sort/ln-sort.schema.json +104 -104
  149. package/components/ln-sort/src/ln-sort.js +321 -321
  150. package/components/ln-sort/src/sort-model.js +117 -117
  151. package/components/ln-sortable/ln-sortable-dev.scss +13 -13
  152. package/components/ln-sortable/ln-sortable.js +1 -1
  153. package/components/ln-sortable/ln-sortable.schema.json +36 -36
  154. package/components/ln-stat/ln-stat-dev.scss +8 -8
  155. package/components/ln-stat/ln-stat.js +1 -1
  156. package/components/ln-stat/ln-stat.schema.json +32 -32
  157. package/components/ln-stat/src/stat-model.js +28 -28
  158. package/components/ln-table/ln-table.js +1 -1
  159. package/components/ln-table/ln-table.schema.json +211 -211
  160. package/components/ln-table/src/ln-table.js +2 -0
  161. package/components/ln-table/src/table-model.js +98 -98
  162. package/components/ln-table-coordinator/ln-table-coordinator.js +1 -1
  163. package/components/ln-table-coordinator/ln-table-coordinator.schema.json +93 -93
  164. package/components/ln-table-coordinator/src/ln-table-coordinator.js +3 -3
  165. package/components/ln-tabs/README.md +2 -2
  166. package/components/ln-tabs/ln-tabs.js +1 -1
  167. package/components/ln-tabs/ln-tabs.schema.json +75 -75
  168. package/components/ln-tabs/src/ln-tabs.js +208 -207
  169. package/components/ln-tabs/src/tabs-model.js +82 -82
  170. package/components/ln-time/ln-time-dev.scss +13 -13
  171. package/components/ln-time/ln-time.js +1 -1
  172. package/components/ln-time/ln-time.schema.json +38 -38
  173. package/components/ln-toast/ln-toast.js +1 -1
  174. package/components/ln-toast/ln-toast.schema.json +65 -65
  175. package/components/ln-toggle/ln-toggle.js +1 -1
  176. package/components/ln-toggle/ln-toggle.schema.json +52 -52
  177. package/components/ln-tooltip/ln-tooltip.js +1 -1
  178. package/components/ln-tooltip/ln-tooltip.schema.json +86 -86
  179. package/components/ln-translations/ln-translations.js +1 -1
  180. package/components/ln-translations/ln-translations.schema.json +120 -120
  181. package/components/ln-ui-coordinator/README.md +62 -62
  182. package/components/ln-ui-coordinator/ln-ui-coordinator.js +1 -1
  183. package/components/ln-ui-coordinator/ln-ui-coordinator.schema.json +28 -28
  184. package/components/ln-ui-coordinator/src/ln-ui-coordinator.js +140 -140
  185. package/components/ln-upload/ln-upload.js +1 -1
  186. package/components/ln-upload/ln-upload.schema.json +193 -193
  187. package/components/ln-upload/src/upload-model.js +78 -78
  188. package/components/ln-validate/ln-validate.js +1 -1
  189. package/components/ln-validate/ln-validate.schema.json +50 -50
  190. package/components/ln-validate/src/validate-model.js +64 -64
  191. package/components/ln-websocket-connector/README.md +166 -166
  192. package/components/ln-websocket-connector/ln-websocket-connector.js +1 -1
  193. package/components/ln-websocket-connector/ln-websocket-connector.schema.json +29 -29
  194. package/components/ln-websocket-connector/src/ln-websocket-connector.js +283 -279
  195. package/ln-ashlar.html-data.json +7 -8
  196. package/package.json +84 -84
  197. package/theme/brand.css +73 -73
  198. package/theme/components/_confirm.scss +6 -6
  199. package/theme/components/_picklist.scss +3 -3
  200. package/theme/config/_palette.scss +274 -274
  201. package/theme/config/_theme.scss +292 -292
  202. package/theme/config/_tokens.scss +285 -285
  203. package/theme/config/mixins/_index.scss +62 -62
  204. package/theme/config/mixins/_picklist.scss +66 -66
  205. package/theme/ln-ashlar-dev.scss +66 -66
  206. package/theme/ln-ashlar-landing.scss +56 -56
  207. package/theme/ln-ashlar-theme.scss +63 -63
  208. package/theme/presentation/_bento.scss +151 -151
  209. package/theme/presentation/_cta.scss +111 -111
  210. package/theme/presentation/_effects.scss +216 -216
  211. package/theme/presentation/_features.scss +108 -108
  212. package/theme/presentation/_footer.scss +129 -129
  213. package/theme/presentation/_hero.scss +158 -158
  214. package/theme/presentation/_layout.scss +170 -170
  215. package/theme/presentation/_navbar.scss +140 -140
  216. package/theme/presentation/_pricing.scss +262 -262
  217. package/theme/presentation/_stats.scss +87 -87
  218. package/theme/presentation/_testimonials.scss +124 -124
  219. package/theme/presentation/_tokens.scss +39 -39
  220. package/theme/presentation/_typography.scss +163 -163
  221. package/web-types.json +12 -12
@@ -1,155 +1,184 @@
1
- import { compareValues, detectValueType } from '../../ln-core/compare.js';
2
- import { matchesFilterValues, matchesSearchTokens, tokenizeSearchQuery } from '../../ln-core/matching.js';
3
-
4
- /**
5
- * Sorts an array of record objects by field and direction using pure core comparison.
6
- * @param {Array<Record<string, any>>} records
7
- * @param {{ field?: string, direction?: string } | null} [sort]
8
- * @param {Intl.Collator} [collator]
9
- * @returns {Array<Record<string, any>>}
10
- */
11
- export function sortRecords(records, sort, collator) {
12
- if (!Array.isArray(records) || !sort || !sort.field) return records;
13
- const { field, direction } = sort;
14
- const desc = direction === 'desc';
15
-
16
- // Lock the value type once for the entire sort operation (per Doctrinal / compare.js rules)
17
- const sampleValues = records.map(r => (r ? r[field] : undefined));
18
- const valueType = detectValueType(sampleValues);
19
-
20
- return [...records].sort((a, b) => {
21
- const va = a ? a[field] : undefined;
22
- const vb = b ? b[field] : undefined;
23
-
24
- const result = compareValues(va, vb, valueType, collator);
25
- return desc ? -result : result;
26
- });
27
- }
28
-
29
- /**
30
- * Filters an array of records against a dictionary of active filter arrays.
31
- * Reuses matchesFilterValues from ln-core/matching (AND across fields, OR within field).
32
- * @param {Array<Record<string, any>>} records
33
- * @param {Record<string, Array<any>>} filters
34
- * @returns {Array<Record<string, any>>}
35
- */
36
- export function filterRecords(records, filters) {
37
- if (!Array.isArray(records) || !filters || typeof filters !== 'object') return records;
38
-
39
- const keys = Object.keys(filters).filter(k => Array.isArray(filters[k]) && filters[k].length > 0);
40
- if (!keys.length) return records;
41
-
42
- return records.filter(record => {
43
- if (!record) return false;
44
- return keys.every(field => matchesFilterValues(record[field], filters[field]));
45
- });
46
- }
47
-
48
- /**
49
- * Searches records across specified fields using ln-core/matching tokenization and matching.
50
- * @param {Array<Record<string, any>>} records
51
- * @param {string} query
52
- * @param {string[]} searchFields
53
- * @returns {Array<Record<string, any>>}
54
- */
55
- export function searchRecords(records, query, searchFields) {
56
- if (!Array.isArray(records) || !query || !searchFields || !searchFields.length) return records;
57
-
58
- const tokens = tokenizeSearchQuery(query);
59
- if (!tokens.length) return records;
60
-
61
- return records.filter(record => {
62
- if (!record) return false;
63
- return tokens.every(token =>
64
- searchFields.some(field => {
65
- const val = record[field];
66
- return val != null && matchesSearchTokens(String(val), [token]);
67
- })
68
- );
69
- });
70
- }
71
-
72
- /**
73
- * Computes aggregates (count, sum, avg) on a specific field across records.
74
- * @param {Array<Record<string, any>>} records
75
- * @param {string} field
76
- * @param {'count' | 'sum' | 'avg'} fn
77
- * @returns {number}
78
- */
79
- export function aggregateRecords(records, field, fn) {
80
- if (!Array.isArray(records) || !records.length) return 0;
81
- if (fn === 'count') return records.length;
82
-
83
- const numbers = records
84
- .map(r => (r && r[field] != null ? parseFloat(r[field]) : NaN))
85
- .filter(v => Number.isFinite(v));
86
-
87
- const sum = numbers.reduce((a, b) => a + b, 0);
88
-
89
- if (fn === 'sum') return sum;
90
- if (fn === 'avg') return numbers.length ? sum / numbers.length : 0;
91
- return 0;
92
- }
93
-
94
- /**
95
- * Executes full in-memory query evaluation: filter -> search -> sort -> slice.
96
- * @param {Array<Record<string, any>>} records
97
- * @param {{ filters?: Record<string, any[]>, search?: string, sort?: { field?: string, direction?: string }, offset?: number, limit?: number }} [options={}]
98
- * @param {string[]} [searchFields=[]]
99
- * @param {Intl.Collator} [collator]
100
- * @returns {{ records: Array<Record<string, any>>, total: number, filtered: number }}
101
- */
102
- export function queryRecords(records, options = {}, searchFields = [], collator) {
103
- if (!Array.isArray(records)) {
104
- return { records: [], total: 0, filtered: 0 };
105
- }
106
-
107
- const total = records.length;
108
- let result = records;
109
-
110
- if (options.filters) {
111
- result = filterRecords(result, options.filters);
112
- }
113
- if (options.search) {
114
- result = searchRecords(result, options.search, searchFields);
115
- }
116
-
117
- const filtered = result.length;
118
-
119
- if (options.sort) {
120
- result = sortRecords(result, options.sort, collator);
121
- }
122
-
123
- if (options.offset || options.limit) {
124
- const offset = options.offset || 0;
125
- const limit = options.limit || result.length;
126
- result = result.slice(offset, offset + limit);
127
- }
128
-
129
- return { records: result, total, filtered };
130
- }
131
-
132
- /**
133
- * Decorates records with computed fields (pure, no console side-effects).
134
- * @param {Array<Record<string, any>>} records
135
- * @param {Record<string, (record: any) => any>} [computed]
136
- * @returns {Array<Record<string, any>>}
137
- */
138
- export function decorateRecords(records, computed) {
139
- if (!Array.isArray(records) || !computed || typeof computed !== 'object') return records;
140
-
141
- return records.map(record => {
142
- if (!record) return null;
143
- const copy = { ...record };
144
- for (const [fieldName, fn] of Object.entries(computed)) {
145
- if (typeof fn === 'function') {
146
- try {
147
- copy[fieldName] = fn(record);
148
- } catch (_) {
149
- copy[fieldName] = undefined;
150
- }
151
- }
152
- }
153
- return copy;
154
- });
155
- }
1
+ import { compareValues, detectValueType } from '../../ln-core/compare.js';
2
+ import { matchesFilterValues, matchesSearchTokens, tokenizeSearchQuery } from '../../ln-core/matching.js';
3
+
4
+ /**
5
+ * Sorts an array of record objects by field and direction using pure core comparison.
6
+ * @param {Array<Record<string, any>>} records
7
+ * @param {{ field?: string, direction?: string } | null} [sort]
8
+ * @param {Intl.Collator} [collator]
9
+ * @returns {Array<Record<string, any>>}
10
+ */
11
+ export function sortRecords(records, sort, collator) {
12
+ if (!Array.isArray(records) || !sort || !sort.field) return records;
13
+ const { field, direction } = sort;
14
+ const desc = direction === 'desc';
15
+
16
+ // Lock the value type once for the entire sort operation (per Doctrinal / compare.js rules)
17
+ const sampleValues = records.map(r => (r ? r[field] : undefined));
18
+ const valueType = detectValueType(sampleValues);
19
+
20
+ return [...records].sort((a, b) => {
21
+ const va = a ? a[field] : undefined;
22
+ const vb = b ? b[field] : undefined;
23
+
24
+ const result = compareValues(va, vb, valueType, collator);
25
+ return desc ? -result : result;
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Filters an array of records against a dictionary of active filter arrays.
31
+ * Reuses matchesFilterValues from ln-core/matching (AND across fields, OR within field).
32
+ * @param {Array<Record<string, any>>} records
33
+ * @param {Record<string, Array<any>>} filters
34
+ * @returns {Array<Record<string, any>>}
35
+ */
36
+ export function filterRecords(records, filters) {
37
+ if (!Array.isArray(records) || !filters || typeof filters !== 'object') return records;
38
+
39
+ const keys = Object.keys(filters).filter(k => Array.isArray(filters[k]) && filters[k].length > 0);
40
+ if (!keys.length) return records;
41
+
42
+ return records.filter(record => {
43
+ if (!record) return false;
44
+ return keys.every(field => matchesFilterValues(record[field], filters[field]));
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Searches records across specified fields using ln-core/matching tokenization and matching.
50
+ * @param {Array<Record<string, any>>} records
51
+ * @param {string} query
52
+ * @param {string[]} searchFields
53
+ * @returns {Array<Record<string, any>>}
54
+ */
55
+ export function searchRecords(records, query, searchFields) {
56
+ if (!Array.isArray(records) || !query || !searchFields || !searchFields.length) return records;
57
+
58
+ const tokens = tokenizeSearchQuery(query);
59
+ if (!tokens.length) return records;
60
+
61
+ return records.filter(record => {
62
+ if (!record) return false;
63
+ return tokens.every(token =>
64
+ searchFields.some(field => {
65
+ const val = record[field];
66
+ return val != null && matchesSearchTokens(String(val), [token]);
67
+ })
68
+ );
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Computes aggregates (count, sum, avg) on a specific field across records.
74
+ * @param {Array<Record<string, any>>} records
75
+ * @param {string} field
76
+ * @param {'count' | 'sum' | 'avg'} fn
77
+ * @returns {number}
78
+ */
79
+ export function aggregateRecords(records, field, fn) {
80
+ if (!Array.isArray(records) || !records.length) return 0;
81
+ if (fn === 'count') return records.length;
82
+
83
+ const numbers = records
84
+ .map(r => (r && r[field] != null ? parseFloat(r[field]) : NaN))
85
+ .filter(v => Number.isFinite(v));
86
+
87
+ const sum = numbers.reduce((a, b) => a + b, 0);
88
+
89
+ if (fn === 'sum') return sum;
90
+ if (fn === 'avg') return numbers.length ? sum / numbers.length : 0;
91
+ return 0;
92
+ }
93
+
94
+ /**
95
+ * Executes full in-memory query evaluation: filter -> search -> sort -> slice.
96
+ * @param {Array<Record<string, any>>} records
97
+ * @param {{ filters?: Record<string, any[]>, search?: string, sort?: { field?: string, direction?: string }, offset?: number, limit?: number }} [options={}]
98
+ * @param {string[]} [searchFields=[]]
99
+ * @param {Intl.Collator} [collator]
100
+ * @returns {{ records: Array<Record<string, any>>, total: number, filtered: number }}
101
+ */
102
+ export function queryRecords(records, options = {}, searchFields = [], collator) {
103
+ if (!Array.isArray(records)) {
104
+ return { records: [], total: 0, filtered: 0 };
105
+ }
106
+
107
+ const total = records.length;
108
+ let result = records;
109
+
110
+ if (options.filters) {
111
+ result = filterRecords(result, options.filters);
112
+ }
113
+ if (options.search) {
114
+ result = searchRecords(result, options.search, searchFields);
115
+ }
116
+
117
+ const filtered = result.length;
118
+
119
+ if (options.sort) {
120
+ result = sortRecords(result, options.sort, collator);
121
+ }
122
+
123
+ if (options.offset || options.limit) {
124
+ const offset = options.offset || 0;
125
+ const limit = options.limit || result.length;
126
+ result = result.slice(offset, offset + limit);
127
+ }
128
+
129
+ return { records: result, total, filtered };
130
+ }
131
+
132
+ /**
133
+ * Decorates records with computed fields (pure, no console side-effects).
134
+ * @param {Array<Record<string, any>>} records
135
+ * @param {Record<string, (record: any) => any>} [computed]
136
+ * @returns {Array<Record<string, any>>}
137
+ */
138
+ export function decorateRecords(records, computed) {
139
+ if (!Array.isArray(records) || !computed || typeof computed !== 'object') return records;
140
+
141
+ return records.map(record => {
142
+ if (!record) return null;
143
+ const copy = { ...record };
144
+ for (const [fieldName, fn] of Object.entries(computed)) {
145
+ if (typeof fn === 'function') {
146
+ try {
147
+ copy[fieldName] = fn(record);
148
+ } catch (_) {
149
+ copy[fieldName] = undefined;
150
+ }
151
+ }
152
+ }
153
+ return copy;
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Pure helper to compute whether an IndexedDB upgrade is needed.
159
+ * @param {string[]} existingStoreNames
160
+ * @param {Record<string, { indexes: string[] }>} requiredStores
161
+ * @param {Record<string, string[]>} existingIndexMap
162
+ * @param {string} [metaStoreName='_meta']
163
+ * @returns {boolean}
164
+ */
165
+ export function checkNeedsUpgrade(existingStoreNames, requiredStores, existingIndexMap, metaStoreName = '_meta') {
166
+ if (!Array.isArray(existingStoreNames) || !existingStoreNames.includes(metaStoreName)) return true;
167
+ for (const name of Object.keys(requiredStores || {})) {
168
+ if (!existingStoreNames.includes(name)) return true;
169
+ const requiredIdxs = (requiredStores[name] && requiredStores[name].indexes) || [];
170
+ const existingIdxs = (existingIndexMap && existingIndexMap[name]) || [];
171
+ if (requiredIdxs.some(idx => !existingIdxs.includes(idx))) return true;
172
+ }
173
+ return false;
174
+ }
175
+
176
+ /**
177
+ * Pure helper to normalize synchronization errors into a standard response shape.
178
+ * @param {any} err
179
+ * @returns {{ ok: false, error: string }}
180
+ */
181
+ export function normalizeSyncError(err) {
182
+ const errorMsg = (err && err.message) ? err.message : String(err);
183
+ return { ok: false, error: errorMsg };
184
+ }
@@ -1,6 +1,6 @@
1
1
  import { registerComponent, dispatch, setCryptoKey, getCryptoKey, encryptData, decryptData, defineAttrs, attrSpec, attrInt, attrBool, attrList } from '../../ln-core';
2
2
  import { createWindowIndex } from './window-index.js';
3
- import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from './data-store-model.js';
3
+ import { aggregateRecords, decorateRecords, filterRecords, queryRecords, checkNeedsUpgrade, normalizeSyncError } from './data-store-model.js';
4
4
 
5
5
  (function () {
6
6
  const DOM_SELECTOR = 'data-ln-data-store';
@@ -66,7 +66,7 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
66
66
 
67
67
  _dbReady = new Promise(resolve => {
68
68
  if (typeof indexedDB === 'undefined') {
69
- console.warn('[ln-data-store] IndexedDB not available — falling back to in-memory store');
69
+ console.warn('[ln-data-store] IndexedDB is not available in this environment');
70
70
  return resolve(null);
71
71
  }
72
72
 
@@ -75,14 +75,24 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
75
75
  const probe = indexedDB.open(DB_NAME);
76
76
 
77
77
  probe.onerror = () => {
78
- console.warn('[ln-data-store] IndexedDB open failed — falling back to in-memory store');
78
+ console.warn('[ln-data-store] IndexedDB open failed');
79
79
  resolve(null);
80
80
  };
81
81
 
82
82
  probe.onsuccess = e => {
83
83
  const db = e.target.result;
84
84
  const existing = Array.from(db.objectStoreNames);
85
- const needsUpgrade = !existing.includes(META_STORE) || requiredNames.some(name => !existing.includes(name));
85
+ const existingRequired = requiredNames.filter(name => existing.includes(name));
86
+
87
+ let existingIndexMap = {};
88
+ if (existingRequired.length > 0) {
89
+ const tx = db.transaction(existingRequired, 'readonly');
90
+ for (const name of existingRequired) {
91
+ existingIndexMap[name] = Array.from(tx.objectStore(name).indexNames);
92
+ }
93
+ }
94
+
95
+ const needsUpgrade = checkNeedsUpgrade(existing, required, existingIndexMap, META_STORE);
86
96
 
87
97
  if (!needsUpgrade) {
88
98
  _setupVersionChangeHandler(db);
@@ -94,18 +104,35 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
94
104
  db.close();
95
105
 
96
106
  const upgrade = indexedDB.open(DB_NAME, currentVersion + 1);
107
+ let settled = false;
108
+ let blockedTimeout = null;
97
109
 
98
110
  upgrade.onblocked = () => {
99
111
  console.warn('[ln-data-store] Database upgrade blocked — waiting for other tabs to close connection');
112
+ dispatch(document, 'ln-data-store:blocked', { db: DB_NAME });
113
+ if (!blockedTimeout) {
114
+ blockedTimeout = setTimeout(() => {
115
+ if (settled) return;
116
+ console.warn('[ln-data-store] Database upgrade timed out while blocked');
117
+ settled = true;
118
+ _dbReady = null;
119
+ resolve(null);
120
+ }, 5000);
121
+ }
100
122
  };
101
123
 
102
124
  upgrade.onerror = () => {
125
+ if (settled) return;
126
+ if (blockedTimeout) clearTimeout(blockedTimeout);
127
+ settled = true;
128
+ _dbReady = null;
103
129
  console.warn('[ln-data-store] Database upgrade failed');
104
130
  resolve(null);
105
131
  };
106
132
 
107
133
  upgrade.onupgradeneeded = e => {
108
134
  const db = e.target.result;
135
+ const tx = e.target.transaction;
109
136
  if (!db.objectStoreNames.contains(META_STORE)) {
110
137
  db.createObjectStore(META_STORE, { keyPath: 'key' });
111
138
  }
@@ -115,11 +142,24 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
115
142
  for (const idx of required[storeName].indexes) {
116
143
  store.createIndex(idx, idx, { unique: false });
117
144
  }
145
+ } else {
146
+ const store = tx.objectStore(storeName);
147
+ for (const idx of required[storeName].indexes) {
148
+ if (!store.indexNames.contains(idx)) {
149
+ store.createIndex(idx, idx, { unique: false });
150
+ }
151
+ }
118
152
  }
119
153
  }
120
154
  };
121
155
 
122
156
  upgrade.onsuccess = e => {
157
+ if (settled) {
158
+ if (e.target.result) e.target.result.close();
159
+ return;
160
+ }
161
+ if (blockedTimeout) clearTimeout(blockedTimeout);
162
+ settled = true;
123
163
  const db = e.target.result;
124
164
  _setupVersionChangeHandler(db);
125
165
  _db = db;
@@ -771,7 +811,7 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
771
811
  _component.prototype.count = function (filters) {
772
812
  const hasFilters = filters && Object.keys(filters).length > 0;
773
813
  if (!hasFilters) {
774
- if (this.totalCount != null) return Promise.resolve(this.totalCount);
814
+ if (this.windowed && this.totalCount != null) return Promise.resolve(this.totalCount);
775
815
  return _countRecords(this._name);
776
816
  }
777
817
  return _getAllRecords(this._name).then(records => filterRecords(records, filters).length);
@@ -791,12 +831,12 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
791
831
  meta = meta || {};
792
832
  const self = this;
793
833
  if (self._windowIndex && meta.queryGen != null && meta.queryGen !== self._windowIndex.queryGen) {
794
- return Promise.resolve();
834
+ return Promise.resolve({ ok: true, stale: true });
795
835
  }
796
836
 
797
- const hasChanges = upsertedRecords.length > 0 || deletedIds.length > 0;
798
-
799
- let chain = Promise.resolve();
837
+ let chain = _getDb().then(db => {
838
+ if (!db) throw new Error('IndexedDB unavailable');
839
+ });
800
840
  if (upsertedRecords.length > 0) chain = chain.then(() => _putBulk(self._name, upsertedRecords));
801
841
  if (deletedIds.length > 0) chain = chain.then(() => _deleteBulk(self._name, deletedIds));
802
842
 
@@ -836,9 +876,16 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
836
876
  meta: meta
837
877
  });
838
878
  }
879
+ return { ok: true };
839
880
  }).catch(err => {
840
881
  self.isSyncing = false;
882
+ const errorMsg = (err && err.message) ? err.message : String(err);
841
883
  console.error('[ln-data-store] applySync failed:', err);
884
+ dispatch(self.dom, 'ln-data-store:sync-error', {
885
+ store: self._name,
886
+ error: errorMsg
887
+ });
888
+ return normalizeSyncError(err);
842
889
  });
843
890
  };
844
891
 
@@ -957,14 +1004,14 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
957
1004
 
958
1005
  // The IndexedDB schema and the store's registered name are fixed at open
959
1006
  // time: createIndex is only legal inside a versionchange transaction
960
- // (see onupgradeneeded, :85-94). A post-init edit is recorded on the host
1007
+ // (see onupgradeneeded). A post-init edit is recorded on the host
961
1008
  // for the dev stylesheet to surface; nothing is applied.
962
1009
  function _markFrozen(el, attrName) {
963
1010
  el.setAttribute(FROZEN_ATTR, attrName);
964
1011
  }
965
1012
 
966
1013
  // Shrinking the window drops positions; the records behind them stop being
967
- // held, exactly as on ingest (:757-758), so they leave IndexedDB too.
1014
+ // held, exactly as on ingest, so they leave IndexedDB too.
968
1015
  function _applyWindowSize(el) {
969
1016
  const inst = el[DOM_ATTRIBUTE];
970
1017
  if (!inst._windowIndex) return;
@@ -207,3 +207,7 @@ On blur: empty → clear + dispatch; unchanged from the current formatted displa
207
207
  ### Observers
208
208
 
209
209
  A shared `document.body` `MutationObserver` auto-initializes new `[data-ln-date]` elements (`childList`) and re-initializes on `data-ln-date` attribute addition (`attributes`). A separate observer on `document.documentElement` watches `lang` and re-formats every active instance when it changes.
210
+
211
+ ### Teardown & Lifecycle (`destroy()`)
212
+
213
+ When an element is destroyed, the component cleanly detaches the global `ln-core:locale-change` event listener regardless of whether the element is an input decorator or a static text display element (`<time>`, `<td>`, `<span>`). For inputs, wrapper children, blur/click listeners, and value interceptors are unhooked to prevent DOM leaks.