@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,131 @@
1
+ import { registerComponent, dispatch } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-confirm';
5
+ const DOM_ATTRIBUTE = 'lnConfirm';
6
+ const TIMEOUT_ATTR = 'data-ln-confirm-timeout';
7
+ const DEFAULT_TIMEOUT = 3;
8
+
9
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
10
+
11
+ // ─── Component ─────────────────────────────────────────────
12
+
13
+ function _component(dom) {
14
+ this.dom = dom;
15
+ this.confirming = false;
16
+ this.originalText = dom.textContent.trim();
17
+ this.confirmText = dom.getAttribute(DOM_SELECTOR) || 'Confirm?';
18
+ this.revertTimer = null;
19
+ this._submitted = false;
20
+
21
+ const self = this;
22
+ this._onClick = function (e) {
23
+ if (!self.confirming) {
24
+ e.preventDefault();
25
+ e.stopImmediatePropagation();
26
+ self._enterConfirm();
27
+ } else {
28
+ if (self._submitted) return;
29
+ self._submitted = true;
30
+ // Second click — allow form submit
31
+ self._reset();
32
+ }
33
+ };
34
+
35
+ dom.addEventListener('click', this._onClick);
36
+ return this;
37
+ }
38
+
39
+ _component.prototype._getTimeout = function () {
40
+ const val = parseFloat(this.dom.getAttribute(TIMEOUT_ATTR));
41
+ return (isNaN(val) || val <= 0) ? DEFAULT_TIMEOUT : val;
42
+ };
43
+
44
+ _component.prototype._enterConfirm = function () {
45
+ this.confirming = true;
46
+ this.dom.setAttribute('data-confirming', 'true');
47
+
48
+ var iconUse = this.dom.querySelector('svg.ln-icon use');
49
+ if (iconUse && this.originalText === '') {
50
+ this.isIconButton = true;
51
+ this.originalIconHref = iconUse.getAttribute('href');
52
+ iconUse.setAttribute('href', '#ln-check');
53
+ this.dom.classList.add('ln-confirm-tooltip');
54
+ this.dom.setAttribute('data-tooltip-text', this.confirmText);
55
+ // Accessibility — swap aria-label so the new accessible name
56
+ // matches the prompt, and append a transient role="alert"
57
+ // announcer so AT speaks the prompt immediately (aria-label
58
+ // mutation alone is not announced on a focused element).
59
+ this.originalAriaLabel = this.dom.getAttribute('aria-label');
60
+ this.dom.setAttribute('aria-label', this.confirmText);
61
+ this.alertNode = document.createElement('span');
62
+ this.alertNode.className = 'sr-only';
63
+ this.alertNode.setAttribute('role', 'alert');
64
+ this.alertNode.textContent = this.confirmText;
65
+ this.dom.appendChild(this.alertNode);
66
+ } else {
67
+ this.dom.textContent = this.confirmText;
68
+ }
69
+
70
+ this._startTimer();
71
+
72
+ dispatch(this.dom, 'ln-confirm:waiting', { target: this.dom });
73
+ };
74
+
75
+ _component.prototype._startTimer = function () {
76
+ if (this.revertTimer) {
77
+ clearTimeout(this.revertTimer);
78
+ }
79
+ const self = this;
80
+ const ms = this._getTimeout() * 1000;
81
+ this.revertTimer = setTimeout(function () {
82
+ self._reset();
83
+ }, ms);
84
+ };
85
+
86
+ _component.prototype._reset = function () {
87
+ this._submitted = false;
88
+ this.confirming = false;
89
+ this.dom.removeAttribute('data-confirming');
90
+
91
+ if (this.isIconButton) {
92
+ var iconUse = this.dom.querySelector('svg.ln-icon use');
93
+ if (iconUse && this.originalIconHref) {
94
+ iconUse.setAttribute('href', this.originalIconHref);
95
+ }
96
+ this.dom.classList.remove('ln-confirm-tooltip');
97
+ this.dom.removeAttribute('data-tooltip-text');
98
+ // Accessibility — restore aria-label and remove the announcer.
99
+ if (this.originalAriaLabel !== null && this.originalAriaLabel !== undefined) {
100
+ this.dom.setAttribute('aria-label', this.originalAriaLabel);
101
+ } else {
102
+ this.dom.removeAttribute('aria-label');
103
+ }
104
+ this.originalAriaLabel = null;
105
+ if (this.alertNode && this.alertNode.parentNode === this.dom) {
106
+ this.dom.removeChild(this.alertNode);
107
+ }
108
+ this.alertNode = null;
109
+ this.isIconButton = false;
110
+ this.originalIconHref = null;
111
+ } else {
112
+ this.dom.textContent = this.originalText;
113
+ }
114
+
115
+ if (this.revertTimer) {
116
+ clearTimeout(this.revertTimer);
117
+ this.revertTimer = null;
118
+ }
119
+ };
120
+
121
+ _component.prototype.destroy = function () {
122
+ if (!this.dom[DOM_ATTRIBUTE]) return;
123
+ this._reset();
124
+ this.dom.removeEventListener('click', this._onClick);
125
+ delete this.dom[DOM_ATTRIBUTE];
126
+ };
127
+
128
+ // ─── Init ──────────────────────────────────────────────────
129
+
130
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-confirm');
131
+ })();
@@ -0,0 +1,83 @@
1
+ // Web Crypto API Reusable Cryptographic Helpers
2
+ let _cryptoKey = null;
3
+
4
+ export async function setCryptoKey(secretString) {
5
+ if (!secretString) {
6
+ _cryptoKey = null;
7
+ return;
8
+ }
9
+ try {
10
+ const enc = new TextEncoder();
11
+ const hash = await crypto.subtle.digest('SHA-256', enc.encode(secretString));
12
+ _cryptoKey = await crypto.subtle.importKey(
13
+ 'raw',
14
+ hash,
15
+ { name: 'AES-GCM' },
16
+ false,
17
+ ['encrypt', 'decrypt']
18
+ );
19
+ } catch (err) {
20
+ console.error('[ln-core/crypto] Key derivation failed:', err);
21
+ _cryptoKey = null;
22
+ }
23
+ }
24
+
25
+ export function getCryptoKey() {
26
+ return _cryptoKey;
27
+ }
28
+
29
+ export async function encryptData(plainData, key = _cryptoKey) {
30
+ const activeKey = key || _cryptoKey;
31
+ if (!activeKey || plainData === undefined || plainData === null) return plainData;
32
+
33
+ try {
34
+ const enc = new TextEncoder();
35
+ const iv = crypto.getRandomValues(new Uint8Array(12));
36
+
37
+ const serialized = typeof plainData === 'string' ? plainData : JSON.stringify(plainData);
38
+ const encryptedBuffer = await crypto.subtle.encrypt(
39
+ { name: 'AES-GCM', iv: iv },
40
+ activeKey,
41
+ enc.encode(serialized)
42
+ );
43
+
44
+ const ivBase64 = btoa(String.fromCharCode(...iv));
45
+ const dataBase64 = btoa(String.fromCharCode(...new Uint8Array(encryptedBuffer)));
46
+
47
+ return {
48
+ encrypted: true,
49
+ iv: ivBase64,
50
+ data: dataBase64
51
+ };
52
+ } catch (err) {
53
+ console.error('[ln-core/crypto] Encryption failed:', err);
54
+ return plainData;
55
+ }
56
+ }
57
+
58
+ export async function decryptData(encryptedObject, key = _cryptoKey) {
59
+ const activeKey = key || _cryptoKey;
60
+ if (!encryptedObject || !encryptedObject.encrypted || !activeKey) return encryptedObject;
61
+
62
+ try {
63
+ const dec = new TextDecoder();
64
+ const iv = Uint8Array.from(atob(encryptedObject.iv), c => c.charCodeAt(0));
65
+ const encryptedBuffer = Uint8Array.from(atob(encryptedObject.data), c => c.charCodeAt(0));
66
+
67
+ const decryptedBuffer = await crypto.subtle.decrypt(
68
+ { name: 'AES-GCM', iv: iv },
69
+ activeKey,
70
+ encryptedBuffer
71
+ );
72
+
73
+ const decoded = dec.decode(decryptedBuffer);
74
+ try {
75
+ return JSON.parse(decoded);
76
+ } catch (_) {
77
+ return decoded;
78
+ }
79
+ } catch (err) {
80
+ console.error('[ln-core/crypto] Decryption failed. Key may be incorrect:', err);
81
+ return { ...encryptedObject, decryptionError: true };
82
+ }
83
+ }
@@ -0,0 +1,411 @@
1
+ // ─── Template Cache ────────────────────────────────────────
2
+ const _tmplCache = {};
3
+
4
+ /**
5
+ * Clone a <template data-ln-template="name"> element.
6
+ * Cached after first lookup.
7
+ */
8
+ export function cloneTemplate(name, componentTag) {
9
+ if (!_tmplCache[name]) {
10
+ _tmplCache[name] = document.querySelector('[data-ln-template="' + name + '"]');
11
+ }
12
+ const tmpl = _tmplCache[name];
13
+ if (!tmpl) {
14
+ console.warn('[' + (componentTag || 'ln-core') + '] Template "' + name + '" not found');
15
+ return null;
16
+ }
17
+ return tmpl.content.cloneNode(true);
18
+ }
19
+
20
+ // ─── Event Dispatch ────────────────────────────────────────
21
+
22
+ export function dispatch(element, eventName, detail) {
23
+ element.dispatchEvent(new CustomEvent(eventName, {
24
+ bubbles: true,
25
+ detail: detail || {}
26
+ }));
27
+ }
28
+
29
+ export function dispatchCancelable(element, eventName, detail) {
30
+ const event = new CustomEvent(eventName, {
31
+ bubbles: true,
32
+ cancelable: true,
33
+ detail: detail || {}
34
+ });
35
+ element.dispatchEvent(event);
36
+ return event;
37
+ }
38
+
39
+ // ─── Declarative DOM Binding ───────────────────────────────
40
+
41
+ export function fill(root, data) {
42
+ if (!root || !data) return root;
43
+
44
+ // data-ln-field="prop" → textContent
45
+ const fields = root.querySelectorAll('[data-ln-field]');
46
+ for (let i = 0; i < fields.length; i++) {
47
+ const el = fields[i];
48
+ const prop = el.getAttribute('data-ln-field');
49
+ if (data[prop] != null) {
50
+ el.textContent = data[prop];
51
+ }
52
+ }
53
+
54
+ // data-ln-attr="attr:prop, attr:prop" → setAttribute
55
+ const attrs = root.querySelectorAll('[data-ln-attr]');
56
+ for (let i = 0; i < attrs.length; i++) {
57
+ const el = attrs[i];
58
+ const pairs = el.getAttribute('data-ln-attr').split(',');
59
+ for (let j = 0; j < pairs.length; j++) {
60
+ const parts = pairs[j].trim().split(':');
61
+ if (parts.length !== 2) continue;
62
+ const attr = parts[0].trim();
63
+ const prop = parts[1].trim();
64
+ if (data[prop] != null) {
65
+ el.setAttribute(attr, data[prop]);
66
+ }
67
+ }
68
+ }
69
+
70
+ // data-ln-show="prop" → classList.toggle('hidden', !value)
71
+ const shows = root.querySelectorAll('[data-ln-show]');
72
+ for (let i = 0; i < shows.length; i++) {
73
+ const el = shows[i];
74
+ const prop = el.getAttribute('data-ln-show');
75
+ if (prop in data) {
76
+ el.classList.toggle('hidden', !data[prop]);
77
+ }
78
+ }
79
+
80
+ // data-ln-class="cls:prop, cls:prop" → classList.toggle(cls, !!value)
81
+ const classes = root.querySelectorAll('[data-ln-class]');
82
+ for (let i = 0; i < classes.length; i++) {
83
+ const el = classes[i];
84
+ const pairs = el.getAttribute('data-ln-class').split(',');
85
+ for (let j = 0; j < pairs.length; j++) {
86
+ const parts = pairs[j].trim().split(':');
87
+ if (parts.length !== 2) continue;
88
+ const cls = parts[0].trim();
89
+ const prop = parts[1].trim();
90
+ if (prop in data) {
91
+ el.classList.toggle(cls, !!data[prop]);
92
+ }
93
+ }
94
+ }
95
+
96
+ return root;
97
+ }
98
+
99
+ // ─── Template Text-Node Placeholders ──────────────────────
100
+
101
+ export function fillTemplate(clone, data) {
102
+ if (!clone || !data) return clone;
103
+ const walker = document.createTreeWalker(clone, NodeFilter.SHOW_TEXT);
104
+ while (walker.nextNode()) {
105
+ const node = walker.currentNode;
106
+ if (node.textContent.indexOf('{{') !== -1) {
107
+ node.textContent = node.textContent.replace(
108
+ /\{\{\s*(\w+)\s*\}\}/g,
109
+ function (_, key) { return data[key] !== undefined ? data[key] : ''; }
110
+ );
111
+ }
112
+ }
113
+ return clone;
114
+ }
115
+
116
+ // ─── Keyed List Rendering ──────────────────────────────────
117
+
118
+ export function renderList(container, items, templateName, keyFn, fillFn, componentTag) {
119
+ // Index existing children by data-ln-key
120
+ const existingByKey = {};
121
+ for (let i = 0; i < container.children.length; i++) {
122
+ const child = container.children[i];
123
+ const key = child.getAttribute('data-ln-key');
124
+ if (key) existingByKey[key] = child;
125
+ }
126
+
127
+ // Build ordered fragment — reuse or clone
128
+ const frag = document.createDocumentFragment();
129
+
130
+ for (let i = 0; i < items.length; i++) {
131
+ const item = items[i];
132
+ const key = String(keyFn(item));
133
+ let el = existingByKey[key];
134
+
135
+ if (el) {
136
+ fillFn(el, item, i);
137
+ } else {
138
+ const clone = cloneTemplate(templateName, componentTag);
139
+ if (!clone) continue;
140
+ fillTemplate(clone, item);
141
+ el = clone.firstElementChild;
142
+ if (!el) continue;
143
+ el.setAttribute('data-ln-key', key);
144
+ fillFn(el, item, i);
145
+ }
146
+ frag.appendChild(el);
147
+ }
148
+
149
+ // Atomic update: orphans still in container, reused nodes already moved to frag
150
+ container.textContent = '';
151
+ container.appendChild(frag);
152
+ }
153
+
154
+ // ─── Guard Body ────────────────────────────────────────
155
+
156
+ export function guardBody(setupFn, componentTag) {
157
+ if (!document.body) {
158
+ document.addEventListener('DOMContentLoaded', function () {
159
+ guardBody(setupFn, componentTag);
160
+ });
161
+ console.warn('[' + componentTag + '] Script loaded before <body> — add "defer" to your <script> tag');
162
+ return;
163
+ }
164
+ setupFn();
165
+ }
166
+
167
+ export function cloneTemplateScoped(root, name, componentTag) {
168
+ if (root) {
169
+ const local = root.querySelector('[data-ln-template="' + name + '"]');
170
+ if (local) return local.content.cloneNode(true);
171
+ }
172
+ return cloneTemplate(name, componentTag);
173
+ }
174
+
175
+ // ─── Dictionary (i18n) ────────────────────────────────────
176
+
177
+ /**
178
+ * Build a plain object from hidden dictionary elements.
179
+ * Reads all [selector] elements once, extracts key→textContent,
180
+ * removes them from DOM. Returns dictionary object.
181
+ *
182
+ * HTML: <ul hidden>
183
+ * <li data-ln-upload-dict="remove">Remove</li>
184
+ * </ul>
185
+ * JS: const dict = buildDict(container, 'data-ln-upload-dict');
186
+ * dict.remove → 'Remove'
187
+ */
188
+ export function buildDict(root, selector) {
189
+ const dict = {};
190
+ const els = root.querySelectorAll('[' + selector + ']');
191
+ for (let i = 0; i < els.length; i++) {
192
+ dict[els[i].getAttribute(selector)] = els[i].textContent;
193
+ els[i].remove();
194
+ }
195
+ return dict;
196
+ }
197
+
198
+ // ─── Find Elements ─────────────────────────────────────────
199
+
200
+ export function findElements(root, selector, attribute, ComponentClass) {
201
+ if (root.nodeType !== 1) return;
202
+
203
+ // Support both simple attribute names and full CSS selectors
204
+ const isComplex = selector.indexOf('[') !== -1 || selector.indexOf('.') !== -1 || selector.indexOf('#') !== -1;
205
+ const query = isComplex ? selector : '[' + selector + ']';
206
+
207
+ const items = Array.from(root.querySelectorAll(query));
208
+ if (root.matches && root.matches(query)) {
209
+ items.push(root);
210
+ }
211
+ for (const el of items) {
212
+ if (!el[attribute]) {
213
+ el[attribute] = new ComponentClass(el);
214
+ }
215
+ }
216
+ }
217
+
218
+ // ─── Visibility Check ─────────────────────────────────────
219
+
220
+ export function isVisible(el) {
221
+ return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
222
+ }
223
+
224
+ // ─── Form Serialization ───────────────────────────────────
225
+
226
+ export function serializeForm(form) {
227
+ const data = {};
228
+ const elements = form.elements;
229
+
230
+ for (let i = 0; i < elements.length; i++) {
231
+ const el = elements[i];
232
+ if (!el.name || el.disabled || el.type === 'file' || el.type === 'submit' || el.type === 'button') continue;
233
+
234
+ if (el.type === 'checkbox') {
235
+ if (!data[el.name]) data[el.name] = [];
236
+ if (el.checked) data[el.name].push(el.value);
237
+ } else if (el.type === 'radio') {
238
+ if (el.checked) data[el.name] = el.value;
239
+ } else if (el.type === 'select-multiple') {
240
+ data[el.name] = [];
241
+ for (let j = 0; j < el.options.length; j++) {
242
+ if (el.options[j].selected) data[el.name].push(el.options[j].value);
243
+ }
244
+ } else {
245
+ data[el.name] = el.value;
246
+ }
247
+ }
248
+
249
+ return data;
250
+ }
251
+
252
+ export function populateForm(form, data) {
253
+ const elements = form.elements;
254
+ const filled = [];
255
+
256
+ for (let i = 0; i < elements.length; i++) {
257
+ const el = elements[i];
258
+ if (!el.name || !(el.name in data) || el.type === 'file' || el.type === 'submit' || el.type === 'button') continue;
259
+
260
+ const value = data[el.name];
261
+
262
+ if (el.type === 'checkbox') {
263
+ el.checked = Array.isArray(value) ? value.indexOf(el.value) !== -1 : !!value;
264
+ filled.push(el);
265
+ } else if (el.type === 'radio') {
266
+ el.checked = el.value === String(value);
267
+ filled.push(el);
268
+ } else if (el.type === 'select-multiple') {
269
+ if (Array.isArray(value)) {
270
+ for (let j = 0; j < el.options.length; j++) {
271
+ el.options[j].selected = value.indexOf(el.options[j].value) !== -1;
272
+ }
273
+ }
274
+ filled.push(el);
275
+ } else {
276
+ el.value = value;
277
+ filled.push(el);
278
+ }
279
+ }
280
+
281
+ return filled;
282
+ }
283
+
284
+ // ─── Locale Detection ─────────────────────────────────────
285
+
286
+ export function getLocale(el) {
287
+ const langEl = el.closest('[lang]');
288
+ return (langEl ? langEl.lang : null) || navigator.language;
289
+ }
290
+
291
+ // ─── Component Registration ───────────────────────────────
292
+
293
+ export function registerComponent(selector, attribute, ComponentFn, componentTag, options = {}) {
294
+ const extraAttributes = options.extraAttributes || [];
295
+ const onAttributeChange = options.onAttributeChange || null;
296
+ const onInit = options.onInit || null;
297
+
298
+ function constructor(domRoot) {
299
+ const root = domRoot || document.body;
300
+ findElements(root, selector, attribute, ComponentFn);
301
+ if (onInit) onInit(root);
302
+ }
303
+
304
+ guardBody(function () {
305
+ const observer = new MutationObserver(function (mutations) {
306
+ for (let i = 0; i < mutations.length; i++) {
307
+ const mutation = mutations[i];
308
+ if (mutation.type === 'childList') {
309
+ for (let j = 0; j < mutation.addedNodes.length; j++) {
310
+ const node = mutation.addedNodes[j];
311
+ if (node.nodeType === 1) {
312
+ findElements(node, selector, attribute, ComponentFn);
313
+ if (onInit) onInit(node);
314
+ }
315
+ }
316
+ } else if (mutation.type === 'attributes') {
317
+ if (onAttributeChange && mutation.target[attribute]) {
318
+ onAttributeChange(mutation.target, mutation.attributeName);
319
+ } else {
320
+ findElements(mutation.target, selector, attribute, ComponentFn);
321
+ if (onInit) onInit(mutation.target);
322
+ }
323
+ }
324
+ }
325
+ });
326
+
327
+ // Extract attribute names from selector for attributeFilter
328
+ let observedAttributes = [];
329
+ if (selector.indexOf('[') !== -1) {
330
+ const re = /\[([\w-]+)/g;
331
+ let match;
332
+ while ((match = re.exec(selector)) !== null) {
333
+ observedAttributes.push(match[1]);
334
+ }
335
+ } else {
336
+ observedAttributes.push(selector);
337
+ }
338
+
339
+ observer.observe(document.body, {
340
+ childList: true,
341
+ subtree: true,
342
+ attributes: true,
343
+ attributeFilter: observedAttributes.concat(extraAttributes)
344
+ });
345
+ }, componentTag || (selector.indexOf('[') === -1 ? selector.replace('data-', '') : 'component'));
346
+
347
+ window[attribute] = constructor;
348
+
349
+ if (document.readyState === 'loading') {
350
+ document.addEventListener('DOMContentLoaded', function () {
351
+ constructor(document.body);
352
+ });
353
+ } else {
354
+ constructor(document.body);
355
+ }
356
+
357
+ return constructor;
358
+ }
359
+
360
+ // ─── HTTP / URL Helpers ────────────────────────────────────
361
+
362
+ /**
363
+ * Build a URL from segments, stripping duplicate slashes.
364
+ */
365
+ export function buildUrl(...segments) {
366
+ return segments
367
+ .filter(x => x !== undefined && x !== null && x !== '')
368
+ .map((part, index) => {
369
+ if (index === 0) return part.replace(/\/+$/, '');
370
+ return part.replace(/^\/+/, '').replace(/\/+$/, '');
371
+ })
372
+ .filter(Boolean)
373
+ .join('/');
374
+ }
375
+
376
+ /**
377
+ * Compile standard JSON request headers.
378
+ */
379
+ export function getHeaders(customHeaders, auth) {
380
+ return Object.assign({
381
+ 'Content-Type': 'application/json',
382
+ 'Accept': 'application/json'
383
+ }, customHeaders, auth ? { 'Authorization': auth } : null);
384
+ }
385
+
386
+ /**
387
+ * Safely parse headers from JSON string.
388
+ */
389
+ export function parseHeaders(str, componentName = 'ln-core') {
390
+ try { return str ? JSON.parse(str) : {}; }
391
+ catch (e) { return console.error(`[${componentName}] Invalid headers JSON:`, e), {}; }
392
+ }
393
+
394
+ // ─── Domain Data Mapper Registry ───────────────────────────
395
+ const _dataMappers = {};
396
+
397
+ export function registerDataMapper(name, mapper) {
398
+ _dataMappers[name] = mapper;
399
+ }
400
+
401
+ export function getDataMapper(name) {
402
+ return _dataMappers[name] || { ingress: r => r, egress: r => r };
403
+ }
404
+
405
+ if (typeof window !== 'undefined') {
406
+ window.lnCore = window.lnCore || {};
407
+ window.lnCore.registerDataMapper = registerDataMapper;
408
+ window.lnCore.getDataMapper = getDataMapper;
409
+ }
410
+
411
+
@@ -0,0 +1,5 @@
1
+ export { cloneTemplate, cloneTemplateScoped, dispatch, dispatchCancelable, fill, fillTemplate, renderList, buildDict, guardBody, findElements, isVisible, serializeForm, populateForm, getLocale, registerComponent, buildUrl, getHeaders, parseHeaders, registerDataMapper, getDataMapper } from './helpers.js';
2
+ export { reactiveState, deepReactive, createBatcher } from './reactive.js';
3
+ export { persistGet, persistSet, persistRemove, persistClear } from './persist.js';
4
+ export { computePlacement, teleportToBody, measureHidden } from './positioning.js';
5
+ export { setCryptoKey, getCryptoKey, encryptData, decryptData } from './crypto.js';
@@ -0,0 +1,71 @@
1
+ // ─── Persist (localStorage) ───────────────────────────────
2
+ //
3
+ // Key format: ln:{component}:{pathname}:{elementId}
4
+ // Example: ln:filter:/admin/users:status-filter
5
+ //
6
+ // Each component passes its own name ("filter", "table-sort", "toggle", "tabs"),
7
+ // so keys are namespaced per component — no cross-component collision risk.
8
+ // Element ID comes from data-ln-persist="key" or el.id.
9
+ //
10
+ // Note: ln-autosave uses its own separate prefix ("ln-autosave:") and does NOT
11
+ // go through this module. No collision between the two systems.
12
+
13
+ const PREFIX = 'ln:';
14
+
15
+ function _pageKey() {
16
+ const path = location.pathname.replace(/\/+$/, '').toLowerCase();
17
+ return path || '/';
18
+ }
19
+
20
+ function _resolveKey(component, el) {
21
+ const persist = el.getAttribute('data-ln-persist');
22
+ const id = (persist !== null && persist !== '') ? persist : el.id;
23
+ if (!id) {
24
+ console.warn('[ln-persist] Element requires id or data-ln-persist="key"', el);
25
+ return null;
26
+ }
27
+ return PREFIX + component + ':' + _pageKey() + ':' + id;
28
+ }
29
+
30
+ export function persistGet(component, el) {
31
+ const key = _resolveKey(component, el);
32
+ if (!key) return null;
33
+ try {
34
+ const raw = localStorage.getItem(key);
35
+ return raw !== null ? JSON.parse(raw) : null;
36
+ } catch (e) {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ export function persistSet(component, el, value) {
42
+ const key = _resolveKey(component, el);
43
+ if (!key) return;
44
+ try {
45
+ localStorage.setItem(key, JSON.stringify(value));
46
+ } catch (e) {
47
+ // localStorage full or disabled — silent
48
+ }
49
+ }
50
+
51
+ export function persistRemove(component, el) {
52
+ const key = _resolveKey(component, el);
53
+ if (!key) return;
54
+ try {
55
+ localStorage.removeItem(key);
56
+ } catch (e) {}
57
+ }
58
+
59
+ export function persistClear(component) {
60
+ try {
61
+ const prefix = PREFIX + component + ':';
62
+ const toRemove = [];
63
+ for (let i = 0; i < localStorage.length; i++) {
64
+ const k = localStorage.key(i);
65
+ if (k && k.indexOf(prefix) === 0) toRemove.push(k);
66
+ }
67
+ for (let i = 0; i < toRemove.length; i++) {
68
+ localStorage.removeItem(toRemove[i]);
69
+ }
70
+ } catch (e) {}
71
+ }