@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,177 @@
1
+ import { guardBody } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-nav';
5
+ const DOM_ATTRIBUTE = 'lnNav';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ const navInstances = new WeakMap();
10
+
11
+ // ─── pushState singleton patch ──────────────────────────────
12
+ const _pushStateCallbacks = [];
13
+
14
+ if (!history._lnNavPatched) {
15
+ const _origPushState = history.pushState;
16
+ history.pushState = function () {
17
+ _origPushState.apply(history, arguments);
18
+ for (const cb of _pushStateCallbacks) { cb(); }
19
+ };
20
+ history._lnNavPatched = true;
21
+ }
22
+
23
+ // ─── Constructor ───────────────────────────────────────────
24
+
25
+ function constructor(navElement) {
26
+ if (!navElement.hasAttribute(DOM_SELECTOR)) return;
27
+ if (navInstances.has(navElement)) return;
28
+
29
+ const activeClass = navElement.getAttribute(DOM_SELECTOR);
30
+ if (!activeClass) return;
31
+
32
+ const instance = _initializeNav(navElement, activeClass);
33
+ navInstances.set(navElement, instance);
34
+ navElement[DOM_ATTRIBUTE] = instance;
35
+ }
36
+
37
+ function _initializeNav(navElement, activeClass) {
38
+ let links = Array.from(navElement.querySelectorAll('a'));
39
+
40
+ _updateActiveState(links, activeClass, window.location.pathname);
41
+
42
+ const updateHandler = function () {
43
+ links = Array.from(navElement.querySelectorAll('a'));
44
+ _updateActiveState(links, activeClass, window.location.pathname);
45
+ };
46
+
47
+ window.addEventListener('popstate', updateHandler);
48
+ _pushStateCallbacks.push(updateHandler);
49
+
50
+ const observer = new MutationObserver(function (mutations) {
51
+ for (const mutation of mutations) {
52
+ if (mutation.type === 'childList') {
53
+ for (const node of mutation.addedNodes) {
54
+ if (node.nodeType === 1) {
55
+ if (node.tagName === 'A') {
56
+ links.push(node);
57
+ _updateActiveState([node], activeClass, window.location.pathname);
58
+ } else if (node.querySelectorAll) {
59
+ const newLinks = Array.from(node.querySelectorAll('a'));
60
+ links = links.concat(newLinks);
61
+ _updateActiveState(newLinks, activeClass, window.location.pathname);
62
+ }
63
+ }
64
+ }
65
+
66
+ for (const node of mutation.removedNodes) {
67
+ if (node.nodeType === 1) {
68
+ if (node.tagName === 'A') {
69
+ links = links.filter(function (link) { return link !== node; });
70
+ } else if (node.querySelectorAll) {
71
+ const removedLinks = Array.from(node.querySelectorAll('a'));
72
+ links = links.filter(function (link) {
73
+ return !removedLinks.includes(link);
74
+ });
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ });
81
+
82
+ observer.observe(navElement, { childList: true, subtree: true });
83
+
84
+ return {
85
+ navElement: navElement,
86
+ activeClass: activeClass,
87
+ observer: observer,
88
+ updateHandler: updateHandler,
89
+ destroy: function () {
90
+ observer.disconnect();
91
+ window.removeEventListener('popstate', updateHandler);
92
+ const idx = _pushStateCallbacks.indexOf(updateHandler);
93
+ if (idx !== -1) _pushStateCallbacks.splice(idx, 1);
94
+ navInstances.delete(navElement);
95
+ delete navElement[DOM_ATTRIBUTE];
96
+ }
97
+ };
98
+ }
99
+
100
+ function _normalizeUrl(url) {
101
+ try {
102
+ const urlObj = new URL(url, window.location.href);
103
+ return urlObj.pathname.replace(/\/$/, '') || '/';
104
+ } catch (e) {
105
+ return url.replace(/\/$/, '') || '/';
106
+ }
107
+ }
108
+
109
+ function _updateActiveState(links, activeClass, currentPath) {
110
+ const normalizedCurrent = _normalizeUrl(currentPath);
111
+
112
+ for (const link of links) {
113
+ const href = link.getAttribute('href');
114
+ if (!href) continue;
115
+
116
+ const normalizedHref = _normalizeUrl(href);
117
+
118
+ link.classList.remove(activeClass);
119
+
120
+ const isExact = normalizedHref === normalizedCurrent;
121
+ const isParent = normalizedHref !== '/' && normalizedCurrent.startsWith(normalizedHref + '/');
122
+
123
+ if (isExact || isParent) {
124
+ link.classList.add(activeClass);
125
+ }
126
+ }
127
+ }
128
+
129
+ // ─── Global DOM Observer ───────────────────────────────────
130
+
131
+ function _domObserver() {
132
+ guardBody(function () {
133
+ const observer = new MutationObserver(function (mutations) {
134
+ for (const mutation of mutations) {
135
+ if (mutation.type === 'childList') {
136
+ for (const node of mutation.addedNodes) {
137
+ if (node.nodeType === 1) {
138
+ if (node.hasAttribute && node.hasAttribute(DOM_SELECTOR)) {
139
+ constructor(node);
140
+ }
141
+ if (node.querySelectorAll) {
142
+ for (const el of node.querySelectorAll('[' + DOM_SELECTOR + ']')) {
143
+ constructor(el);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ } else if (mutation.type === 'attributes') {
149
+ if (mutation.target.hasAttribute && mutation.target.hasAttribute(DOM_SELECTOR)) {
150
+ constructor(mutation.target);
151
+ }
152
+ }
153
+ }
154
+ });
155
+
156
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [DOM_SELECTOR] });
157
+ }, 'ln-nav');
158
+ }
159
+
160
+ // ─── Init ──────────────────────────────────────────────────
161
+
162
+ window[DOM_ATTRIBUTE] = constructor;
163
+
164
+ function _initializeAll() {
165
+ for (const el of document.querySelectorAll('[' + DOM_SELECTOR + ']')) {
166
+ constructor(el);
167
+ }
168
+ }
169
+
170
+ _domObserver();
171
+
172
+ if (document.readyState === 'loading') {
173
+ document.addEventListener('DOMContentLoaded', _initializeAll);
174
+ } else {
175
+ _initializeAll();
176
+ }
177
+ })();
@@ -0,0 +1,122 @@
1
+ # ln-number
2
+
3
+ Real-time locale-aware number formatting for input fields.
4
+
5
+ ## Usage
6
+
7
+ ```html
8
+ <input type="number" name="amount" data-ln-number>
9
+ ```
10
+
11
+ The component creates a hidden input that holds the raw numeric value for form submission and formats the visible input with locale-aware thousand separators. See [`docs/js/number.md`](../../docs/js/number.md#html) for the resulting DOM.
12
+
13
+ ## Loading & Source Files
14
+
15
+ ### Loading the Component
16
+
17
+ #### 1. In-Bundle (Standard Integration)
18
+ To load `ln-number` as part of the main `ln-ashlar` bundle, include the compiled bundle script:
19
+ ```html
20
+ <script src="dist/ln-ashlar.iife.js" defer></script>
21
+ ```
22
+
23
+ #### 2. Standalone (Zero-Dependency IIFE)
24
+ To load `ln-number` as a standalone component, include its compiled IIFE under the component directory:
25
+ ```html
26
+ <script src="js/ln-number/ln-number.js" defer></script>
27
+ ```
28
+
29
+ ### Source Files
30
+
31
+ * **Active Development Source**: [js/ln-number/src/ln-number.js](file:///c:/laragon/www/ln-ashlar/js/ln-number/src/ln-number.js) (source of truth)
32
+ * **Compiled Standalone**: [js/ln-number/ln-number.js](file:///c:/laragon/www/ln-ashlar/js/ln-number/ln-number.js)
33
+
34
+ ## Attributes
35
+
36
+ | Attribute | On | Description |
37
+ |-----------|-----|-------------|
38
+ | `data-ln-number` | `<input>` | Enables number formatting |
39
+ | `data-ln-number-decimals` | `<input>` | Max decimal places (default: unlimited) |
40
+ | `data-ln-number-min` | `<input>` | Minimum allowed value |
41
+ | `data-ln-number-max` | `<input>` | Maximum allowed value |
42
+
43
+ ## Events
44
+
45
+ | Event | Bubbles | Cancelable | Detail |
46
+ |-------|---------|------------|--------|
47
+ | `ln-number:input` | yes | no | `{ value: Number, formatted: String }` |
48
+ | `ln-number:destroyed` | yes | no | `{ target: Element }` |
49
+
50
+ ## API
51
+
52
+ ```javascript
53
+ const el = document.querySelector('[data-ln-number]');
54
+
55
+ el.lnNumber.value; // get raw number (Number or NaN if empty)
56
+ el.lnNumber.value = 1234.56; // set value programmatically — formats display
57
+ el.lnNumber.formatted; // get formatted display string
58
+
59
+ el.lnNumber.destroy(); // remove component, restore original input
60
+ ```
61
+
62
+ ## Locale
63
+
64
+ The component reads the nearest ancestor `[lang]` attribute (typically `<html lang>`); falls back to `navigator.language`. Locale changes propagate live — re-formatting all instances when `<html lang>` changes.
65
+
66
+ | `lang` | Display |
67
+ |---|---|
68
+ | `mk` | `1.234.567` |
69
+ | `en-US` | `1,234,567` |
70
+
71
+ ## Examples
72
+
73
+ ```html
74
+ <!-- Basic -->
75
+ <div class="form-element">
76
+ <label for="amount">Amount</label>
77
+ <input type="number" id="amount" name="amount" data-ln-number>
78
+ </div>
79
+
80
+ <!-- With decimal limit -->
81
+ <div class="form-element">
82
+ <label for="price">Price</label>
83
+ <input type="number" id="price" name="price"
84
+ data-ln-number data-ln-number-decimals="2">
85
+ </div>
86
+
87
+ <!-- With min/max -->
88
+ <div class="form-element">
89
+ <label for="quantity">Quantity</label>
90
+ <input type="number" id="quantity" name="quantity"
91
+ data-ln-number data-ln-number-min="0" data-ln-number-max="999999">
92
+ </div>
93
+
94
+ <!-- Pre-filled value -->
95
+ <div class="form-element">
96
+ <label for="budget">Budget</label>
97
+ <input type="number" id="budget" name="budget" value="1500000"
98
+ data-ln-number>
99
+ </div>
100
+ ```
101
+
102
+ ## Integration with ln-validate
103
+
104
+ Place `data-ln-validate` on the same input. The `required` attribute stays
105
+ on the visible input and works as expected:
106
+
107
+ ```html
108
+ <div class="form-element">
109
+ <label for="salary">Salary</label>
110
+ <input type="number" id="salary" name="salary"
111
+ required data-ln-validate data-ln-number>
112
+ <ul data-ln-validate-errors>
113
+ <li class="hidden" data-ln-validate-error="required">Required field</li>
114
+ </ul>
115
+ </div>
116
+ ```
117
+
118
+ ## Integration with ln-form
119
+
120
+ Works automatically. `serializeForm()` reads the hidden input (which has the
121
+ `name`). `populateForm()` sets the hidden input's value, which triggers the
122
+ display update.
@@ -0,0 +1 @@
1
+ (function(){"use strict";function N(a,i,u){a.dispatchEvent(new CustomEvent(i,{bubbles:!0,detail:u||{}}))}function x(a,i){if(!document.body){document.addEventListener("DOMContentLoaded",function(){x(a,i)}),console.warn("["+i+'] Script loaded before <body> — add "defer" to your <script> tag');return}a()}function S(a,i,u,g){if(a.nodeType!==1)return;const v=i.indexOf("[")!==-1||i.indexOf(".")!==-1||i.indexOf("#")!==-1?i:"["+i+"]",f=Array.from(a.querySelectorAll(v));a.matches&&a.matches(v)&&f.push(a);for(const m of f)m[u]||(m[u]=new g(m))}function w(a){const i=a.closest("[lang]");return(i?i.lang:null)||navigator.language}function O(a,i,u,g,b={}){const v=b.extraAttributes||[],f=b.onAttributeChange||null,m=b.onInit||null;function t(o){const e=o||document.body;S(e,a,i,u),m&&m(e)}return x(function(){const o=new MutationObserver(function(l){for(let r=0;r<l.length;r++){const n=l[r];if(n.type==="childList")for(let d=0;d<n.addedNodes.length;d++){const p=n.addedNodes[d];p.nodeType===1&&(S(p,a,i,u),m&&m(p))}else n.type==="attributes"&&(f&&n.target[i]?f(n.target,n.attributeName):(S(n.target,a,i,u),m&&m(n.target)))}});let e=[];if(a.indexOf("[")!==-1){const l=/\[([\w-]+)/g;let r;for(;(r=l.exec(a))!==null;)e.push(r[1])}else e.push(a);o.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:e.concat(v)})},g),window[i]=t,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){t(document.body)}):t(document.body),t}const A={};function I(a,i){A[a]=i}function C(a){return A[a]||{ingress:i=>i,egress:i=>i}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=I,window.lnCore.getDataMapper=C),(function(){const a="data-ln-number",i="lnNumber";if(window[i]!==void 0)return;const u={},g=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value");function b(t){if(!u[t]){const o=new Intl.NumberFormat(t,{useGrouping:!0}),e=o.formatToParts(1234.5);let l="",r=".";for(let n=0;n<e.length;n++)e[n].type==="group"&&(l=e[n].value),e[n].type==="decimal"&&(r=e[n].value);u[t]={fmt:o,groupSep:l,decimalSep:r}}return u[t]}function v(t,o,e){if(e!==null){const l=parseInt(e,10),r=t+"|d"+l;return u[r]||(u[r]=new Intl.NumberFormat(t,{useGrouping:!0,minimumFractionDigits:0,maximumFractionDigits:l})),u[r].format(o)}return b(t).fmt.format(o)}function f(t){if(t.tagName!=="INPUT")return console.warn("[ln-number] Can only be applied to <input>, got:",t.tagName),this;this.dom=t;const o=document.createElement("input");o.type="hidden",o.name=t.name,t.removeAttribute("name"),t.type="text",t.setAttribute("inputmode","decimal"),t.insertAdjacentElement("afterend",o),this._hidden=o;const e=this;Object.defineProperty(o,"value",{get:function(){return g.get.call(o)},set:function(r){g.set.call(o,r),r!==""&&!isNaN(parseFloat(r))?e._displayFormatted(parseFloat(r)):r===""&&(e.dom.value="")}}),this._onInput=function(){e._handleInput()},t.addEventListener("input",this._onInput),this._onPaste=function(r){r.preventDefault();const n=(r.clipboardData||window.clipboardData).getData("text"),d=b(w(t)),p=d.decimalSep.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");let c=n.replace(new RegExp("[^0-9\\-"+p+".]","g"),"");d.groupSep&&(c=c.split(d.groupSep).join("")),d.decimalSep!=="."&&(c=c.replace(d.decimalSep,"."));const _=parseFloat(c);isNaN(_)?(t.value="",e._hidden.value=""):e.value=_},t.addEventListener("paste",this._onPaste);const l=t.value;if(l!==""){const r=parseFloat(l);isNaN(r)||(this._displayFormatted(r),g.set.call(o,String(r)))}return this}f.prototype._handleInput=function(){const t=this.dom,o=b(w(t)),e=t.value;if(e===""){this._hidden.value="",N(t,"ln-number:input",{value:NaN,formatted:""});return}if(e==="-"){this._hidden.value="";return}const l=t.selectionStart;let r=0;for(let s=0;s<l;s++)/[0-9]/.test(e[s])&&r++;let n=e;if(o.groupSep&&(n=n.split(o.groupSep).join("")),n=n.replace(o.decimalSep,"."),e.endsWith(o.decimalSep)||e.endsWith(".")){const s=n.replace(/\.$/,""),h=parseFloat(s);isNaN(h)||this._setHiddenRaw(h);return}const d=n.indexOf(".");if(d!==-1&&n.slice(d+1).endsWith("0")){const h=parseFloat(n);isNaN(h)||this._setHiddenRaw(h);return}const p=t.getAttribute("data-ln-number-decimals");if(p!==null&&d!==-1){const s=parseInt(p,10);n.slice(d+1).length>s&&(n=n.slice(0,d+1+s))}const c=parseFloat(n);if(isNaN(c))return;const _=t.getAttribute("data-ln-number-min"),E=t.getAttribute("data-ln-number-max");if(_!==null&&c<parseFloat(_)||E!==null&&c>parseFloat(E))return;let y;if(p!==null)y=v(w(t),c,p);else{const s=d!==-1?n.slice(d+1).length:0;if(s>0){const h=w(t)+"|u"+s;u[h]||(u[h]=new Intl.NumberFormat(w(t),{useGrouping:!0,minimumFractionDigits:s,maximumFractionDigits:s})),y=u[h].format(c)}else y=o.fmt.format(c)}t.value=y;let F=r,D=0;for(let s=0;s<y.length&&F>0;s++)D=s+1,/[0-9]/.test(y[s])&&F--;F>0&&(D=y.length),t.setSelectionRange(D,D),this._setHiddenRaw(c),N(t,"ln-number:input",{value:c,formatted:y})},f.prototype._setHiddenRaw=function(t){g.set.call(this._hidden,String(t))},f.prototype._displayFormatted=function(t){this.dom.value=v(w(this.dom),t,this.dom.getAttribute("data-ln-number-decimals"))},Object.defineProperty(f.prototype,"value",{get:function(){const t=this._hidden.value;return t===""?NaN:parseFloat(t)},set:function(t){if(typeof t!="number"||isNaN(t)){this.dom.value="",this._setHiddenRaw("");return}this._displayFormatted(t),this._setHiddenRaw(t),N(this.dom,"ln-number:input",{value:t,formatted:this.dom.value})}}),Object.defineProperty(f.prototype,"formatted",{get:function(){return this.dom.value}}),f.prototype.destroy=function(){this.dom[i]&&(this.dom.removeEventListener("input",this._onInput),this.dom.removeEventListener("paste",this._onPaste),this.dom.name=this._hidden.name,this.dom.type="number",this.dom.removeAttribute("inputmode"),this._hidden.remove(),N(this.dom,"ln-number:destroyed",{target:this.dom}),delete this.dom[i])};function m(){new MutationObserver(function(){const t=document.querySelectorAll("["+a+"]");for(let o=0;o<t.length;o++){const e=t[o][i];e&&!isNaN(e.value)&&e._displayFormatted(e.value)}}).observe(document.documentElement,{attributes:!0,attributeFilter:["lang"]})}O(a,i,f,"ln-number"),m()})()})();
@@ -0,0 +1,302 @@
1
+ import { dispatch, getLocale, registerComponent } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-number';
5
+ const DOM_ATTRIBUTE = 'lnNumber';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ // ─── Formatter Cache ──────────────────────────────────────
10
+
11
+ const _formatters = {};
12
+ const _inputValueDesc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
13
+
14
+ function _getFormatter(locale) {
15
+ if (!_formatters[locale]) {
16
+ const fmt = new Intl.NumberFormat(locale, { useGrouping: true });
17
+ const parts = fmt.formatToParts(1234.5);
18
+ let groupSep = '';
19
+ let decimalSep = '.';
20
+ for (let i = 0; i < parts.length; i++) {
21
+ if (parts[i].type === 'group') groupSep = parts[i].value;
22
+ if (parts[i].type === 'decimal') decimalSep = parts[i].value;
23
+ }
24
+ _formatters[locale] = { fmt: fmt, groupSep: groupSep, decimalSep: decimalSep };
25
+ }
26
+ return _formatters[locale];
27
+ }
28
+
29
+ function _formatNum(locale, num, maxDecimals) {
30
+ if (maxDecimals !== null) {
31
+ const max = parseInt(maxDecimals, 10);
32
+ const key = locale + '|d' + max;
33
+ if (!_formatters[key]) {
34
+ _formatters[key] = new Intl.NumberFormat(locale, { useGrouping: true, minimumFractionDigits: 0, maximumFractionDigits: max });
35
+ }
36
+ return _formatters[key].format(num);
37
+ }
38
+ return _getFormatter(locale).fmt.format(num);
39
+ }
40
+
41
+ // ─── Component ─────────────────────────────────────────────
42
+
43
+ function _component(dom) {
44
+ if (dom.tagName !== 'INPUT') {
45
+ console.warn('[ln-number] Can only be applied to <input>, got:', dom.tagName);
46
+ return this;
47
+ }
48
+
49
+ this.dom = dom;
50
+
51
+ // ── Create hidden input ─────────────────────────────
52
+ const hidden = document.createElement('input');
53
+ hidden.type = 'hidden';
54
+ hidden.name = dom.name;
55
+ dom.removeAttribute('name');
56
+ dom.type = 'text';
57
+ dom.setAttribute('inputmode', 'decimal');
58
+ dom.insertAdjacentElement('afterend', hidden);
59
+ this._hidden = hidden;
60
+
61
+ // ── Intercept programmatic value sets on hidden input ──
62
+ const self = this;
63
+ Object.defineProperty(hidden, 'value', {
64
+ get: function () {
65
+ return _inputValueDesc.get.call(hidden);
66
+ },
67
+ set: function (val) {
68
+ _inputValueDesc.set.call(hidden, val);
69
+ // If set programmatically (e.g., populateForm), update display
70
+ if (val !== '' && !isNaN(parseFloat(val))) {
71
+ self._displayFormatted(parseFloat(val));
72
+ } else if (val === '') {
73
+ self.dom.value = '';
74
+ }
75
+ }
76
+ });
77
+
78
+ // ── Bind input event ────────────────────────────────
79
+ this._onInput = function () {
80
+ self._handleInput();
81
+ };
82
+ dom.addEventListener('input', this._onInput);
83
+
84
+ // ── Bind paste event ────────────────────────────────
85
+ this._onPaste = function (e) {
86
+ e.preventDefault();
87
+ const pasted = (e.clipboardData || window.clipboardData).getData('text');
88
+ const info = _getFormatter(getLocale(dom));
89
+ const decSepEscaped = info.decimalSep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
90
+ // Strip everything except digits, minus, and decimal separators
91
+ let cleaned = pasted.replace(new RegExp('[^0-9\\-' + decSepEscaped + '.]', 'g'), '');
92
+ // Strip group separators before normalizing decimal
93
+ if (info.groupSep) {
94
+ cleaned = cleaned.split(info.groupSep).join('');
95
+ }
96
+ // Normalize: if locale decimal is not '.', replace it
97
+ if (info.decimalSep !== '.') {
98
+ cleaned = cleaned.replace(info.decimalSep, '.');
99
+ }
100
+ const num = parseFloat(cleaned);
101
+ if (!isNaN(num)) {
102
+ self.value = num;
103
+ } else {
104
+ dom.value = '';
105
+ self._hidden.value = '';
106
+ }
107
+ };
108
+ dom.addEventListener('paste', this._onPaste);
109
+
110
+ // ── Handle pre-filled value ─────────────────────────
111
+ const initial = dom.value;
112
+ if (initial !== '') {
113
+ const num = parseFloat(initial);
114
+ if (!isNaN(num)) {
115
+ this._displayFormatted(num);
116
+ _inputValueDesc.set.call(hidden, String(num));
117
+ }
118
+ }
119
+
120
+ return this;
121
+ }
122
+
123
+ _component.prototype._handleInput = function () {
124
+ const dom = this.dom;
125
+ const info = _getFormatter(getLocale(dom));
126
+ const raw = dom.value;
127
+
128
+ // Edge case: empty
129
+ if (raw === '') {
130
+ this._hidden.value = '';
131
+ dispatch(dom, 'ln-number:input', { value: NaN, formatted: '' });
132
+ return;
133
+ }
134
+
135
+ // Edge case: just minus sign
136
+ if (raw === '-') {
137
+ this._hidden.value = '';
138
+ return;
139
+ }
140
+
141
+ // Save cursor context: count digits to the left of cursor
142
+ const cursorPos = dom.selectionStart;
143
+ let digitsBeforeCursor = 0;
144
+ for (let i = 0; i < cursorPos; i++) {
145
+ if (/[0-9]/.test(raw[i])) digitsBeforeCursor++;
146
+ }
147
+
148
+ // Parse: strip group separators, normalize decimal
149
+ let cleaned = raw;
150
+ if (info.groupSep) {
151
+ cleaned = cleaned.split(info.groupSep).join('');
152
+ }
153
+ cleaned = cleaned.replace(info.decimalSep, '.');
154
+
155
+ // Edge case: trailing decimal separator (user about to type decimals)
156
+ if (raw.endsWith(info.decimalSep) || raw.endsWith('.')) {
157
+ const beforeDecimal = cleaned.replace(/\.$/, '');
158
+ const num = parseFloat(beforeDecimal);
159
+ if (!isNaN(num)) {
160
+ this._setHiddenRaw(num);
161
+ }
162
+ return;
163
+ }
164
+
165
+ // Edge case: trailing zeros after decimal (user still typing)
166
+ const decimalIndex = cleaned.indexOf('.');
167
+ if (decimalIndex !== -1) {
168
+ const afterDecimal = cleaned.slice(decimalIndex + 1);
169
+ if (afterDecimal.endsWith('0')) {
170
+ const num = parseFloat(cleaned);
171
+ if (!isNaN(num)) {
172
+ this._setHiddenRaw(num);
173
+ }
174
+ return;
175
+ }
176
+ }
177
+
178
+ // Enforce decimal limit
179
+ const maxDecimals = dom.getAttribute('data-ln-number-decimals');
180
+ if (maxDecimals !== null && decimalIndex !== -1) {
181
+ const allowed = parseInt(maxDecimals, 10);
182
+ const afterDec = cleaned.slice(decimalIndex + 1);
183
+ if (afterDec.length > allowed) {
184
+ cleaned = cleaned.slice(0, decimalIndex + 1 + allowed);
185
+ }
186
+ }
187
+
188
+ const num = parseFloat(cleaned);
189
+ if (isNaN(num)) return;
190
+
191
+ // Enforce min/max
192
+ const minAttr = dom.getAttribute('data-ln-number-min');
193
+ const maxAttr = dom.getAttribute('data-ln-number-max');
194
+ if (minAttr !== null && num < parseFloat(minAttr)) return;
195
+ if (maxAttr !== null && num > parseFloat(maxAttr)) return;
196
+
197
+ // Format
198
+ let formatted;
199
+ if (maxDecimals !== null) {
200
+ formatted = _formatNum(getLocale(dom), num, maxDecimals);
201
+ } else {
202
+ // Preserve the user's decimal places
203
+ const userDecimals = decimalIndex !== -1 ? cleaned.slice(decimalIndex + 1).length : 0;
204
+ if (userDecimals > 0) {
205
+ const key = getLocale(dom) + '|u' + userDecimals;
206
+ if (!_formatters[key]) {
207
+ _formatters[key] = new Intl.NumberFormat(getLocale(dom), { useGrouping: true, minimumFractionDigits: userDecimals, maximumFractionDigits: userDecimals });
208
+ }
209
+ formatted = _formatters[key].format(num);
210
+ } else {
211
+ formatted = info.fmt.format(num);
212
+ }
213
+ }
214
+
215
+ dom.value = formatted;
216
+
217
+ // Restore cursor position
218
+ let targetDigits = digitsBeforeCursor;
219
+ let newPos = 0;
220
+ for (let i = 0; i < formatted.length && targetDigits > 0; i++) {
221
+ newPos = i + 1;
222
+ if (/[0-9]/.test(formatted[i])) targetDigits--;
223
+ }
224
+ // If we didn't consume all digits, put cursor at end
225
+ if (targetDigits > 0) newPos = formatted.length;
226
+ dom.setSelectionRange(newPos, newPos);
227
+
228
+ // Update hidden input (bypass our setter to avoid feedback loop)
229
+ this._setHiddenRaw(num);
230
+
231
+ dispatch(dom, 'ln-number:input', { value: num, formatted: formatted });
232
+ };
233
+
234
+ _component.prototype._setHiddenRaw = function (num) {
235
+ _inputValueDesc.set.call(this._hidden, String(num));
236
+ };
237
+
238
+ _component.prototype._displayFormatted = function (num) {
239
+ this.dom.value = _formatNum(getLocale(this.dom), num, this.dom.getAttribute('data-ln-number-decimals'));
240
+ };
241
+
242
+ // ─── Public API ───────────────────────────────────────────
243
+
244
+ Object.defineProperty(_component.prototype, 'value', {
245
+ get: function () {
246
+ const raw = this._hidden.value;
247
+ return raw === '' ? NaN : parseFloat(raw);
248
+ },
249
+ set: function (num) {
250
+ if (typeof num !== 'number' || isNaN(num)) {
251
+ this.dom.value = '';
252
+ this._setHiddenRaw('');
253
+ return;
254
+ }
255
+ this._displayFormatted(num);
256
+ this._setHiddenRaw(num);
257
+ dispatch(this.dom, 'ln-number:input', {
258
+ value: num,
259
+ formatted: this.dom.value
260
+ });
261
+ }
262
+ });
263
+
264
+ Object.defineProperty(_component.prototype, 'formatted', {
265
+ get: function () {
266
+ return this.dom.value;
267
+ }
268
+ });
269
+
270
+ _component.prototype.destroy = function () {
271
+ if (!this.dom[DOM_ATTRIBUTE]) return;
272
+ this.dom.removeEventListener('input', this._onInput);
273
+ this.dom.removeEventListener('paste', this._onPaste);
274
+ // Restore name to visible input
275
+ this.dom.name = this._hidden.name;
276
+ this.dom.type = 'number';
277
+ this.dom.removeAttribute('inputmode');
278
+ // Remove hidden input
279
+ this._hidden.remove();
280
+ dispatch(this.dom, 'ln-number:destroyed', { target: this.dom });
281
+ delete this.dom[DOM_ATTRIBUTE];
282
+ };
283
+
284
+ // ─── Locale Observer ──────────────────────────────────────
285
+
286
+ function _localeObserver() {
287
+ new MutationObserver(function () {
288
+ const els = document.querySelectorAll('[' + DOM_SELECTOR + ']');
289
+ for (let i = 0; i < els.length; i++) {
290
+ const inst = els[i][DOM_ATTRIBUTE];
291
+ if (inst && !isNaN(inst.value)) {
292
+ inst._displayFormatted(inst.value);
293
+ }
294
+ }
295
+ }).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] });
296
+ }
297
+
298
+ // ─── Init ──────────────────────────────────────────────────
299
+
300
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-number');
301
+ _localeObserver();
302
+ })();