@livenetworks/ashlar 1.4.1 → 1.6.0

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 (219) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +162 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +77 -41
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +189 -99
  66. package/js/ln-filter/ln-filter-dev.scss +8 -0
  67. package/js/ln-filter/ln-filter.js +1 -1
  68. package/js/ln-filter/src/ln-filter.js +22 -7
  69. package/js/ln-form/README.md +212 -49
  70. package/js/ln-form/ln-form.js +1 -1
  71. package/js/ln-form/src/ln-form.js +99 -199
  72. package/js/ln-http/README.md +125 -89
  73. package/js/ln-http/ln-http.js +1 -1
  74. package/js/ln-http/src/ln-http.js +32 -13
  75. package/js/ln-icon/README.md +130 -0
  76. package/js/ln-icon/ln-icon.js +1 -0
  77. package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
  78. package/js/ln-link/README.md +47 -9
  79. package/js/ln-link/ln-link.js +1 -1
  80. package/js/ln-link/src/ln-link.js +22 -6
  81. package/js/ln-list/README.md +129 -0
  82. package/js/ln-list/ln-list.js +1 -0
  83. package/js/ln-list/ln-list.scss +28 -0
  84. package/js/ln-list/src/ln-list.js +1216 -0
  85. package/js/ln-modal/README.md +185 -154
  86. package/js/ln-modal/ln-modal-dev.scss +13 -0
  87. package/js/ln-modal/ln-modal.js +1 -1
  88. package/js/ln-modal/ln-modal.scss +28 -4
  89. package/js/ln-modal/src/ln-modal.js +137 -201
  90. package/js/ln-modal-coordinator/README.md +99 -0
  91. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
  92. package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
  93. package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
  94. package/js/ln-nav/README.md +46 -3
  95. package/js/ln-nav/ln-nav.js +1 -1
  96. package/js/ln-nav/src/ln-nav.js +92 -127
  97. package/js/ln-number/README.md +28 -1
  98. package/js/ln-number/ln-number-dev.scss +13 -0
  99. package/js/ln-number/ln-number.js +1 -1
  100. package/js/ln-number/src/ln-number.js +443 -350
  101. package/js/ln-options/README.md +66 -0
  102. package/js/ln-options/ln-options.js +1 -0
  103. package/js/ln-options/src/ln-options.js +74 -0
  104. package/js/ln-popover/README.md +40 -5
  105. package/js/ln-popover/ln-popover.js +1 -1
  106. package/js/ln-popover/src/ln-popover.js +38 -11
  107. package/js/ln-progress/README.md +413 -442
  108. package/js/ln-progress/ln-progress.js +1 -1
  109. package/js/ln-progress/src/ln-progress.js +98 -150
  110. package/js/ln-router/README.md +432 -0
  111. package/js/ln-router/ln-router.js +1 -0
  112. package/js/ln-router/src/ln-router.js +539 -0
  113. package/js/ln-search/README.md +158 -83
  114. package/js/ln-search/ln-search-dev.scss +8 -0
  115. package/js/ln-search/ln-search.js +1 -1
  116. package/js/ln-search/src/ln-search.js +12 -9
  117. package/js/ln-slug/README.md +110 -0
  118. package/js/ln-slug/ln-slug.js +1 -0
  119. package/js/ln-slug/src/ln-slug.js +74 -0
  120. package/js/ln-sort/README.md +170 -0
  121. package/js/ln-sort/ln-sort-dev.scss +8 -0
  122. package/js/ln-sort/ln-sort.js +1 -0
  123. package/js/ln-sort/src/ln-sort.js +178 -0
  124. package/js/ln-sortable/README.md +133 -95
  125. package/js/ln-sortable/ln-sortable.js +1 -1
  126. package/js/ln-stat/README.md +55 -0
  127. package/js/ln-stat/ln-stat.js +1 -0
  128. package/js/ln-stat/src/ln-stat.js +55 -0
  129. package/js/ln-table/README.md +285 -101
  130. package/js/ln-table/ln-table-dev.scss +17 -0
  131. package/js/ln-table/ln-table.js +1 -1
  132. package/js/ln-table/ln-table.scss +1 -1
  133. package/js/ln-table/src/ln-table.js +1493 -473
  134. package/js/ln-table-coordinator/README.md +73 -0
  135. package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
  136. package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
  137. package/js/ln-tabs/README.md +221 -137
  138. package/js/ln-tabs/ln-tabs-dev.scss +18 -0
  139. package/js/ln-tabs/ln-tabs.js +1 -1
  140. package/js/ln-tabs/src/ln-tabs.js +41 -24
  141. package/js/ln-time/README.md +14 -0
  142. package/js/ln-time/ln-time.js +1 -1
  143. package/js/ln-time/src/ln-time.js +223 -192
  144. package/js/ln-toast/README.md +178 -122
  145. package/js/ln-toast/ln-toast.js +1 -15
  146. package/js/ln-toast/src/ln-toast.js +232 -210
  147. package/js/ln-toast/template.html +16 -12
  148. package/js/ln-toggle/README.md +61 -2
  149. package/js/ln-toggle/ln-toggle-dev.scss +13 -0
  150. package/js/ln-toggle/ln-toggle.js +1 -1
  151. package/js/ln-toggle/src/ln-toggle.js +56 -43
  152. package/js/ln-tooltip/README.md +95 -58
  153. package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
  154. package/js/ln-tooltip/ln-tooltip.js +1 -1
  155. package/js/ln-tooltip/src/ln-tooltip.js +45 -6
  156. package/js/ln-translations/README.md +118 -96
  157. package/js/ln-translations/ln-translations.js +1 -1
  158. package/js/ln-translations/src/ln-translations.js +10 -5
  159. package/js/ln-upload/README.md +85 -10
  160. package/js/ln-upload/ln-upload.js +1 -1
  161. package/js/ln-upload/src/ln-upload.js +26 -10
  162. package/js/ln-validate/README.md +72 -0
  163. package/js/ln-validate/ln-validate-dev.scss +15 -0
  164. package/js/ln-validate/ln-validate.js +1 -1
  165. package/js/ln-validate/src/ln-validate.js +64 -0
  166. package/package.json +61 -55
  167. package/scss/base/_global.scss +2 -2
  168. package/scss/base/_typography.scss +1 -2
  169. package/scss/components/_date.scss +1 -0
  170. package/scss/components/_dropdown.scss +0 -5
  171. package/scss/components/_editor.scss +5 -0
  172. package/scss/components/_filter.scss +26 -0
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +14 -5
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +68 -1
  177. package/scss/components/_toast.scss +68 -41
  178. package/scss/components/_toggle.scss +16 -0
  179. package/scss/config/_density.scss +205 -117
  180. package/scss/config/_icons.scss +10 -9
  181. package/scss/config/_tokens.scss +69 -47
  182. package/scss/config/mixins/_accordion.scss +2 -7
  183. package/scss/config/mixins/_alert.scss +4 -4
  184. package/scss/config/mixins/_breadcrumbs.scss +2 -2
  185. package/scss/config/mixins/_btn.scss +1 -1
  186. package/scss/config/mixins/_card.scss +89 -14
  187. package/scss/config/mixins/_collapsible.scss +3 -0
  188. package/scss/config/mixins/_colors.scss +1 -1
  189. package/scss/config/mixins/_diagnostics.scss +48 -0
  190. package/scss/config/mixins/_display.scss +1 -0
  191. package/scss/config/mixins/_dropdown.scss +3 -5
  192. package/scss/config/mixins/_editor.scss +124 -0
  193. package/scss/config/mixins/_empty-state.scss +7 -0
  194. package/scss/config/mixins/_filter.scss +47 -0
  195. package/scss/config/mixins/_footer.scss +1 -2
  196. package/scss/config/mixins/_form.scss +207 -21
  197. package/scss/config/mixins/_index.scss +4 -1
  198. package/scss/config/mixins/_ln-table.scss +51 -93
  199. package/scss/config/mixins/_modal.scss +149 -66
  200. package/scss/config/mixins/_nav.scss +3 -4
  201. package/scss/config/mixins/_popover.scss +39 -0
  202. package/scss/config/mixins/_sidebar.scss +1 -1
  203. package/scss/config/mixins/_sort.scss +42 -0
  204. package/scss/config/mixins/_table.scss +232 -38
  205. package/scss/config/mixins/_toast.scss +179 -152
  206. package/scss/config/mixins/_tooltip.scss +17 -7
  207. package/scss/config/mixins/_typography.scss +4 -2
  208. package/scss/ln-ashlar-dev.scss +39 -0
  209. package/scss/ln-ashlar.scss +8 -2
  210. package/js/ln-data-table/README.md +0 -110
  211. package/js/ln-data-table/ln-data-table.js +0 -1
  212. package/js/ln-data-table/ln-data-table.scss +0 -10
  213. package/js/ln-data-table/src/ln-data-table.js +0 -1103
  214. package/js/ln-icons/README.md +0 -88
  215. package/js/ln-icons/ln-icons.js +0 -1
  216. package/js/ln-table/ln-table-sort.js +0 -1
  217. package/js/ln-table/src/ln-table-sort.js +0 -168
  218. package/scss/components/_data-table.scss +0 -83
  219. package/scss/config/mixins/_data-table.scss +0 -346
@@ -1,4 +1,4 @@
1
- import { dispatch, getLocale, registerComponent, interceptValueProperty } from '../../ln-core';
1
+ import { dispatch, getLocale, registerComponent, interceptValueProperty, getLocaleFallback, registerLocaleFallback, buildDict } from '../../ln-core';
2
2
 
3
3
  (function () {
4
4
  const DOM_SELECTOR = 'data-ln-date';
@@ -50,11 +50,30 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
50
50
  const hours = date.getHours();
51
51
  const minutes = date.getMinutes();
52
52
 
53
+ let mmmmVal, mmmVal;
54
+ const fallback = getLocaleFallback(locale);
55
+ const langPrefix = (locale || '').toLowerCase().split('-')[0];
56
+ const formatter = _getFormatter(locale, { month: 'long' });
57
+ const resolvedLocale = formatter.resolvedOptions().locale.toLowerCase().split('-')[0];
58
+ const isFallbackNeeded = fallback && resolvedLocale !== langPrefix;
59
+
60
+ if (isFallbackNeeded && fallback.monthsLong) {
61
+ mmmmVal = fallback.monthsLong[month];
62
+ } else {
63
+ mmmmVal = _getFormatter(locale, { month: 'long' }).format(date);
64
+ }
65
+
66
+ if (isFallbackNeeded && fallback.monthsShort) {
67
+ mmmVal = fallback.monthsShort[month];
68
+ } else {
69
+ mmmVal = _getFormatter(locale, { month: 'short' }).format(date);
70
+ }
71
+
53
72
  const tokens = {
54
73
  'yyyy': String(year),
55
74
  'yy': String(year).slice(-2),
56
- 'MMMM': _getFormatter(locale, { month: 'long' }).format(date),
57
- 'MMM': _getFormatter(locale, { month: 'short' }).format(date),
75
+ 'MMMM': mmmmVal,
76
+ 'MMM': mmmVal,
58
77
  'MM': String(month + 1).padStart(2, '0'),
59
78
  'M': String(month + 1),
60
79
  'dd': String(day).padStart(2, '0'),
@@ -71,29 +90,158 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
71
90
  function _formatDate(date, format, locale) {
72
91
  const intlOptions = _getIntlOptions(format);
73
92
  if (intlOptions) {
74
- return _getFormatter(locale, intlOptions).format(date);
93
+ const formatter = _getFormatter(locale, intlOptions);
94
+ const langPrefix = (locale || '').toLowerCase().split('-')[0];
95
+ const resolvedLocale = formatter.resolvedOptions().locale.toLowerCase().split('-')[0];
96
+ const fallback = getLocaleFallback(locale);
97
+
98
+ if (fallback && resolvedLocale !== langPrefix) {
99
+ return _formatCustom(date, 'dd.MM.yyyy', locale);
100
+ }
101
+ return formatter.format(date);
75
102
  }
76
103
  return _formatCustom(date, format, locale);
77
104
  }
78
105
 
106
+ // ─── Component Helpers ────────────────────────────────────
107
+
108
+ function _toISO(date) {
109
+ if (!date) return '';
110
+ const y = date.getFullYear();
111
+ const m = String(date.getMonth() + 1).padStart(2, '0');
112
+ const d = String(date.getDate()).padStart(2, '0');
113
+ return y + '-' + m + '-' + d;
114
+ }
115
+
116
+ function _notifyChange(self, iso, date) {
117
+ dispatch(self.dom, 'ln-date:change', {
118
+ value: iso,
119
+ formatted: self.dom.value,
120
+ date: date
121
+ });
122
+ self.dom.dispatchEvent(new Event('change', { bubbles: true }));
123
+ }
124
+
125
+ function _updateState(self, iso, date, formattedText) {
126
+ self._setHiddenRaw(iso);
127
+ _inputValueDesc.set.call(self._picker, iso);
128
+ self._lastISO = iso;
129
+ if (formattedText !== undefined) {
130
+ self._isFormatting = true;
131
+ self.dom.value = formattedText;
132
+ self._isFormatting = false;
133
+ } else if (date) {
134
+ self._displayFormatted(date);
135
+ }
136
+ _notifyChange(self, iso, date);
137
+ }
138
+
139
+ function _clearState(self) {
140
+ self._setHiddenRaw('');
141
+ _inputValueDesc.set.call(self._picker, '');
142
+ self._isFormatting = true;
143
+ self.dom.value = '';
144
+ self._isFormatting = false;
145
+ self._lastISO = '';
146
+ _notifyChange(self, '', null);
147
+ }
148
+
79
149
  // ─── Component ────────────────────────────────────────────
80
150
 
81
- function _component(dom) {
82
- if (dom.tagName !== 'INPUT') {
83
- console.warn('[ln-date] Can only be applied to <input>, got:', dom.tagName);
84
- return this;
151
+ // ─── Component ────────────────────────────────────────────
152
+
153
+ _component.prototype._initTextElement = function () {
154
+ const dom = this.dom;
155
+ let valAttr = dom.getAttribute('data-ln-value');
156
+ let dateAttr = dom.getAttribute('data-ln-date');
157
+ let datetimeAttr = dom.getAttribute('datetime');
158
+
159
+ let candidate = null;
160
+ if (valAttr !== null && valAttr !== '') {
161
+ candidate = valAttr;
162
+ } else if (datetimeAttr !== null && datetimeAttr !== '') {
163
+ candidate = datetimeAttr;
164
+ } else if (dateAttr !== null && dateAttr !== '' && dateAttr !== 'true' && !KEYWORD_RE.test(dateAttr)) {
165
+ candidate = dateAttr;
166
+ } else {
167
+ candidate = dom.textContent.trim();
168
+ }
169
+
170
+ let date = _parseISO(candidate) || _parseTyped(candidate);
171
+ if (!date && candidate) {
172
+ if (!isNaN(candidate)) {
173
+ const num = Number(candidate);
174
+ date = new Date(num > 1e11 ? num : num * 1000);
175
+ } else {
176
+ date = new Date(candidate);
177
+ }
178
+ }
179
+ if (date && !isNaN(date.getTime())) {
180
+ const iso = _toISO(date);
181
+ this._rawValue = iso;
182
+ if (!dom.hasAttribute('data-ln-value')) {
183
+ dom.setAttribute('data-ln-value', iso);
184
+ }
185
+ this._formatTextContent();
186
+ } else {
187
+ this._rawValue = null;
188
+ }
189
+ };
190
+
191
+ _component.prototype._formatTextContent = function () {
192
+ if (this._rawValue) {
193
+ const date = _parseISO(this._rawValue);
194
+ if (date) {
195
+ const formatAttr = this.dom.getAttribute('data-ln-date-format');
196
+ let format = formatAttr;
197
+ if (!format) {
198
+ const dateAttr = this.dom.getAttribute('data-ln-date');
199
+ if (dateAttr && KEYWORD_RE.test(dateAttr)) {
200
+ format = dateAttr;
201
+ }
202
+ }
203
+ const locale = getLocale(this.dom);
204
+ this.dom.textContent = _formatDate(date, format || 'medium', locale);
205
+ }
85
206
  }
207
+ };
86
208
 
209
+ function _component(dom) {
87
210
  if (dom[DOM_ATTRIBUTE]) return dom[DOM_ATTRIBUTE];
88
211
  dom[DOM_ATTRIBUTE] = this;
89
212
 
90
213
  this.dom = dom;
214
+
215
+ if (dom.tagName !== 'INPUT') {
216
+ this.isTextElement = true;
217
+ this._initTextElement();
218
+ return this;
219
+ }
220
+
221
+ this.isTextElement = false;
91
222
  const self = this;
92
223
 
93
224
  // ── Read initial state ──────────────────────────────
94
- const initialValue = dom.value; // ISO string (YYYY-MM-DD) or empty
225
+ const initialValue = dom.value;
95
226
  const name = dom.name;
96
227
 
228
+ // Check for declarative HTML dictionary nearby
229
+ const container = dom.closest('.form-element, form') || dom.parentNode;
230
+ const dictEls = container.querySelectorAll('[data-ln-date-dict]');
231
+ for (let i = 0; i < dictEls.length; i++) {
232
+ const lang = dictEls[i].getAttribute('data-ln-date-dict');
233
+ if (lang) {
234
+ const dictData = buildDict(dictEls[i], 'data-ln-date-dict-key');
235
+ if (dictData['months-long']) {
236
+ dictData.monthsLong = dictData['months-long'].split(',').map(s => s.trim());
237
+ }
238
+ if (dictData['months-short']) {
239
+ dictData.monthsShort = dictData['months-short'].split(',').map(s => s.trim());
240
+ }
241
+ registerLocaleFallback(lang, dictData);
242
+ }
243
+ }
244
+
97
245
  // ── Wrap field: replace dom with <span data-ln-date-field> and move dom inside ──
98
246
  const wrapper = document.createElement('span');
99
247
  wrapper.setAttribute('data-ln-date-field', '');
@@ -106,6 +254,9 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
106
254
  hidden.type = 'hidden';
107
255
  hidden.name = name;
108
256
  dom.removeAttribute('name');
257
+ if (dom.hasAttribute('data-ln-fill-as')) {
258
+ hidden.setAttribute('data-ln-fill-as', dom.getAttribute('data-ln-fill-as'));
259
+ }
109
260
  dom.insertAdjacentElement('afterend', hidden);
110
261
  this._hidden = hidden;
111
262
 
@@ -123,13 +274,13 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
123
274
  // ── Create calendar button ──────────────────────────
124
275
  const btn = document.createElement('button');
125
276
  btn.type = 'button';
126
- btn.setAttribute('aria-label', 'Open date picker');
127
- btn.innerHTML = '<svg class="ln-icon" aria-hidden="true"><use href="#ln-calendar"></use></svg>';
277
+ btn.setAttribute('aria-label', dom.getAttribute('data-ln-date-label') || 'Open date picker');
278
+ btn.innerHTML = '<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-calendar"></use></svg>';
128
279
  picker.insertAdjacentElement('afterend', btn);
129
280
  this._btn = btn;
130
281
  this._lastISO = '';
131
282
 
132
- // ── Intercept programmatic value sets on hidden input
283
+ // ── Intercept programmatic value sets on hidden input ──
133
284
  Object.defineProperty(hidden, 'value', {
134
285
  get: function () {
135
286
  return _inputValueDesc.get.call(hidden);
@@ -138,27 +289,9 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
138
289
  _inputValueDesc.set.call(hidden, val);
139
290
  if (val && val !== '') {
140
291
  const date = _parseISO(val);
141
- if (date) {
142
- self._displayFormatted(date);
143
- _inputValueDesc.set.call(picker, val);
144
- self._lastISO = val;
145
- dispatch(self.dom, 'ln-date:change', {
146
- value: val,
147
- formatted: self.dom.value,
148
- date: date
149
- });
150
- self.dom.dispatchEvent(new Event('change', { bubbles: true }));
151
- }
292
+ if (date) _updateState(self, val, date);
152
293
  } else if (val === '') {
153
- self.dom.value = '';
154
- _inputValueDesc.set.call(picker, '');
155
- self._lastISO = '';
156
- dispatch(self.dom, 'ln-date:change', {
157
- value: '',
158
- formatted: '',
159
- date: null
160
- });
161
- self.dom.dispatchEvent(new Event('change', { bubbles: true }));
294
+ _clearState(self);
162
295
  }
163
296
  }
164
297
  });
@@ -175,84 +308,34 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
175
308
  }
176
309
  if (!val || val === '') {
177
310
  originalSet('');
178
- self._setHiddenRaw('');
179
- _inputValueDesc.set.call(self._picker, '');
180
- self._lastISO = '';
181
- dispatch(dom, 'ln-date:change', {
182
- value: '',
183
- formatted: '',
184
- date: null
185
- });
186
- dom.dispatchEvent(new Event('change', { bubbles: true }));
311
+ _clearState(self);
187
312
  return;
188
313
  }
189
314
 
190
- // Programmatic value could be an ISO string (YYYY-MM-DD) or a formatted date string
191
- let date = _parseISO(val);
192
- if (!date) {
193
- date = _parseTyped(val);
194
- }
315
+ const date = _parseISO(val) || _parseTyped(val);
195
316
 
196
317
  if (date) {
197
- const y = date.getFullYear();
198
- const m = String(date.getMonth() + 1).padStart(2, '0');
199
- const d = String(date.getDate()).padStart(2, '0');
200
- const iso = y + '-' + m + '-' + d;
201
-
202
- self._setHiddenRaw(iso);
203
- _inputValueDesc.set.call(self._picker, iso);
204
- self._lastISO = iso;
205
-
318
+ const iso = _toISO(date);
206
319
  const format = dom.getAttribute(DOM_SELECTOR) || '';
207
320
  const locale = getLocale(dom);
208
321
  const formatted = _formatDate(date, format, locale);
209
322
  originalSet(formatted);
210
-
211
- dispatch(dom, 'ln-date:change', {
212
- value: iso,
213
- formatted: formatted,
214
- date: date
215
- });
216
- dom.dispatchEvent(new Event('change', { bubbles: true }));
323
+ _updateState(self, iso, date, formatted);
217
324
  } else {
218
325
  originalSet(String(val));
219
- self._setHiddenRaw('');
220
- _inputValueDesc.set.call(self._picker, '');
221
- self._lastISO = '';
222
- dispatch(dom, 'ln-date:change', {
223
- value: '',
224
- formatted: String(val),
225
- date: null
226
- });
227
- dom.dispatchEvent(new Event('change', { bubbles: true }));
326
+ _clearState(self);
228
327
  }
229
328
  }
230
329
  });
231
330
 
232
331
  // ── Bind events ─────────────────────────────────────
233
332
  this._onPickerChange = function () {
234
- const val = picker.value; // ISO YYYY-MM-DD
333
+ const val = picker.value;
235
334
  if (val) {
236
335
  const date = _parseISO(val);
237
- if (date) {
238
- self._setHiddenRaw(val);
239
- self._displayFormatted(date);
240
- self._lastISO = val;
241
- dispatch(self.dom, 'ln-date:change', {
242
- value: val,
243
- formatted: self.dom.value,
244
- date: date
245
- });
246
- }
336
+ if (date) _updateState(self, val, date);
247
337
  } else {
248
- self._setHiddenRaw('');
249
- self.dom.value = '';
250
- self._lastISO = '';
251
- dispatch(self.dom, 'ln-date:change', {
252
- value: '',
253
- formatted: '',
254
- date: null
255
- });
338
+ _clearState(self);
256
339
  }
257
340
  };
258
341
  picker.addEventListener('change', this._onPickerChange);
@@ -260,51 +343,25 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
260
343
  this._onBlur = function () {
261
344
  const typed = self.dom.value.trim();
262
345
 
263
- // Empty input — clear if there was a value
264
346
  if (typed === '') {
265
- if (self._lastISO !== '') {
266
- self._setHiddenRaw('');
267
- _inputValueDesc.set.call(self._picker, '');
268
- self.dom.value = '';
269
- self._lastISO = '';
270
- dispatch(self.dom, 'ln-date:change', {
271
- value: '',
272
- formatted: '',
273
- date: null
274
- });
275
- }
347
+ if (self._lastISO !== '') _clearState(self);
276
348
  return;
277
349
  }
278
350
 
279
- // Check if text is unchanged from current formatted display
280
351
  if (self._lastISO) {
281
352
  const currentDate = _parseISO(self._lastISO);
282
353
  if (currentDate) {
283
354
  const format = self.dom.getAttribute(DOM_SELECTOR) || '';
284
355
  const locale = getLocale(self.dom);
285
- const currentFormatted = _formatDate(currentDate, format, locale);
286
- if (typed === currentFormatted) return;
356
+ if (typed === _formatDate(currentDate, format, locale)) return;
287
357
  }
288
358
  }
289
359
 
290
- // Try to parse the typed value
291
360
  const parsed = _parseTyped(typed);
292
361
  if (parsed) {
293
- const y = parsed.getFullYear();
294
- const m = String(parsed.getMonth() + 1).padStart(2, '0');
295
- const d = String(parsed.getDate()).padStart(2, '0');
296
- const iso = y + '-' + m + '-' + d;
297
- self._setHiddenRaw(iso);
298
- _inputValueDesc.set.call(self._picker, iso);
299
- self._displayFormatted(parsed);
300
- self._lastISO = iso;
301
- dispatch(self.dom, 'ln-date:change', {
302
- value: iso,
303
- formatted: self.dom.value,
304
- date: parsed
305
- });
362
+ const iso = _toISO(parsed);
363
+ _updateState(self, iso, parsed);
306
364
  } else {
307
- // Invalid input — revert to previous display
308
365
  if (self._lastISO) {
309
366
  const prevDate = _parseISO(self._lastISO);
310
367
  if (prevDate) self._displayFormatted(prevDate);
@@ -323,27 +380,15 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
323
380
  // ── Handle pre-filled value ─────────────────────────
324
381
  if (initialValue && initialValue !== '') {
325
382
  const date = _parseISO(initialValue);
326
- if (date) {
327
- this._setHiddenRaw(initialValue);
328
- _inputValueDesc.set.call(picker, initialValue);
329
- this._displayFormatted(date);
330
- this._lastISO = initialValue;
331
- dispatch(dom, 'ln-date:change', {
332
- value: initialValue,
333
- formatted: dom.value,
334
- date: date
335
- });
336
- dom.dispatchEvent(new Event('change', { bubbles: true }));
337
- }
383
+ if (date) _updateState(self, initialValue, date);
338
384
  }
339
385
 
340
386
  return this;
341
387
  }
342
388
 
343
- // ─── Helpers ──────────────────────────────────────────────
389
+ // ─── Parsing Helpers ──────────────────────────────────────
344
390
 
345
391
  function _parseISO(str) {
346
- // Parse YYYY-MM-DD (and optionally YYYY-MM-DDTHH:mm)
347
392
  if (!str || typeof str !== 'string') return null;
348
393
  const parts = str.split('T');
349
394
  const dateParts = parts[0].split('-');
@@ -359,7 +404,6 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
359
404
  min = parseInt(timeParts[1], 10) || 0;
360
405
  }
361
406
  const date = new Date(y, m, d, h, min);
362
- // Validate the date is real (e.g., Feb 30 would roll over)
363
407
  if (date.getFullYear() !== y || date.getMonth() !== m || date.getDate() !== d) return null;
364
408
  return date;
365
409
  }
@@ -367,19 +411,15 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
367
411
  function _parseTyped(str) {
368
412
  if (!str || typeof str !== 'string') return null;
369
413
  str = str.trim();
370
- if (str.length < 6) return null; // minimum: d.M.yy
414
+ if (str.length < 6) return null;
371
415
 
372
- // Detect separator
373
416
  let sep, parts;
374
417
  if (str.indexOf('.') !== -1) {
375
- sep = '.';
376
- parts = str.split('.');
418
+ sep = '.'; parts = str.split('.');
377
419
  } else if (str.indexOf('/') !== -1) {
378
- sep = '/';
379
- parts = str.split('/');
420
+ sep = '/'; parts = str.split('/');
380
421
  } else if (str.indexOf('-') !== -1) {
381
- sep = '-';
382
- parts = str.split('-');
422
+ sep = '-'; parts = str.split('-');
383
423
  } else {
384
424
  return null;
385
425
  }
@@ -393,15 +433,11 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
393
433
  }
394
434
 
395
435
  let day, month, year;
396
-
397
436
  if (sep === '.') {
398
- // dd.MM.yyyy (European)
399
437
  day = nums[0]; month = nums[1]; year = nums[2];
400
438
  } else if (sep === '/') {
401
- // MM/dd/yyyy (US)
402
439
  month = nums[0]; day = nums[1]; year = nums[2];
403
440
  } else {
404
- // dash: yyyy-MM-dd if first part is 4 digits, else dd-MM-yyyy
405
441
  if (parts[0].length === 4) {
406
442
  year = nums[0]; month = nums[1]; day = nums[2];
407
443
  } else {
@@ -409,12 +445,10 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
409
445
  }
410
446
  }
411
447
 
412
- // Two-digit year pivot
413
448
  if (year < 100) {
414
449
  year += (year < 50) ? 2000 : 1900;
415
450
  }
416
451
 
417
- // Validate via Date constructor roundtrip
418
452
  const date = new Date(year, month - 1, day);
419
453
  if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
420
454
  return null;
@@ -427,7 +461,6 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
427
461
  try {
428
462
  this._picker.showPicker();
429
463
  } catch (e) {
430
- // showPicker() can throw if not triggered by user gesture
431
464
  this._picker.click();
432
465
  }
433
466
  } else {
@@ -451,27 +484,34 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
451
484
 
452
485
  Object.defineProperty(_component.prototype, 'value', {
453
486
  get: function () {
487
+ if (this.isTextElement) {
488
+ return this._rawValue || '';
489
+ }
454
490
  return _inputValueDesc.get.call(this._hidden);
455
491
  },
456
492
  set: function (isoStr) {
493
+ if (this.isTextElement) {
494
+ if (!isoStr || isoStr === '') {
495
+ this._rawValue = null;
496
+ this.dom.removeAttribute('data-ln-value');
497
+ this.dom.textContent = '';
498
+ return;
499
+ }
500
+ const date = _parseISO(isoStr) || _parseTyped(isoStr);
501
+ if (!date) return;
502
+ const iso = _toISO(date);
503
+ this._rawValue = iso;
504
+ this.dom.setAttribute('data-ln-value', iso);
505
+ this._formatTextContent();
506
+ return;
507
+ }
457
508
  if (!isoStr || isoStr === '') {
458
- this._setHiddenRaw('');
459
- _inputValueDesc.set.call(this._picker, '');
460
- this.dom.value = '';
461
- this._lastISO = '';
509
+ _clearState(this);
462
510
  return;
463
511
  }
464
512
  const date = _parseISO(isoStr);
465
513
  if (!date) return;
466
- this._setHiddenRaw(isoStr);
467
- _inputValueDesc.set.call(this._picker, isoStr);
468
- this._displayFormatted(date);
469
- this._lastISO = isoStr;
470
- dispatch(this.dom, 'ln-date:change', {
471
- value: isoStr,
472
- formatted: this.dom.value,
473
- date: date
474
- });
514
+ _updateState(this, isoStr, date);
475
515
  }
476
516
  });
477
517
 
@@ -486,38 +526,40 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
486
526
  this.value = '';
487
527
  return;
488
528
  }
489
- const y = dateObj.getFullYear();
490
- const m = String(dateObj.getMonth() + 1).padStart(2, '0');
491
- const d = String(dateObj.getDate()).padStart(2, '0');
492
- this.value = y + '-' + m + '-' + d;
529
+ this.value = _toISO(dateObj);
493
530
  }
494
531
  });
495
532
 
496
533
  Object.defineProperty(_component.prototype, 'formatted', {
497
534
  get: function () {
535
+ if (this.isTextElement) {
536
+ return this.dom.textContent;
537
+ }
498
538
  return this.dom.value;
499
539
  }
500
540
  });
501
541
 
502
542
  _component.prototype.destroy = function () {
503
543
  if (!this.dom[DOM_ATTRIBUTE]) return;
544
+ if (this.isTextElement) {
545
+ dispatch(this.dom, 'ln-date:destroyed', { target: this.dom });
546
+ delete this.dom[DOM_ATTRIBUTE];
547
+ return;
548
+ }
504
549
  this._picker.removeEventListener('change', this._onPickerChange);
505
550
  this.dom.removeEventListener('blur', this._onBlur);
506
551
  this._btn.removeEventListener('click', this._onBtnClick);
507
- // Restore original input
508
- this.dom.name = this._hidden.name;
509
- this.dom.type = 'date';
510
552
  const isoVal = this.value;
511
- // Remove created elements
512
553
  this._hidden.remove();
513
554
  this._picker.remove();
514
555
  this._btn.remove();
515
- // Unwrap: move dom back out and drop the wrapper
516
556
  if (this._wrapper && this._wrapper.parentNode) {
517
557
  this._wrapper.parentNode.insertBefore(this.dom, this._wrapper);
518
558
  this._wrapper.remove();
519
559
  }
520
- // Restore value
560
+ delete this.dom.value;
561
+ this.dom.name = this._hidden.name;
562
+ this.dom.type = 'date';
521
563
  if (isoVal) this.dom.value = isoVal;
522
564
  dispatch(this.dom, 'ln-date:destroyed', { target: this.dom });
523
565
  delete this.dom[DOM_ATTRIBUTE];
@@ -530,16 +572,32 @@ import { dispatch, getLocale, registerComponent, interceptValueProperty } from '
530
572
  const els = document.querySelectorAll('[' + DOM_SELECTOR + ']');
531
573
  for (let i = 0; i < els.length; i++) {
532
574
  const inst = els[i][DOM_ATTRIBUTE];
533
- if (inst && inst.value) {
534
- const date = _parseISO(inst.value);
535
- if (date) inst._displayFormatted(date);
575
+ if (inst) {
576
+ if (inst.isTextElement) {
577
+ inst._formatTextContent();
578
+ } else if (inst.value) {
579
+ const date = _parseISO(inst.value);
580
+ if (date) inst._displayFormatted(date);
581
+ }
536
582
  }
537
583
  }
538
- }).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] });
584
+ }).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'], subtree: true });
539
585
  }
540
586
 
541
587
  // ─── Init ─────────────────────────────────────────────────
542
588
 
543
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-date');
589
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-date', {
590
+ extraAttributes: ['data-ln-date-format', 'data-ln-date-locale', 'data-ln-value', 'datetime', 'lang'],
591
+ onAttributeChange: function (el) {
592
+ const inst = el[DOM_ATTRIBUTE];
593
+ if (!inst) return;
594
+ if (inst.isTextElement) {
595
+ inst._initTextElement();
596
+ } else if (inst.value) {
597
+ const date = _parseISO(inst.value);
598
+ if (date) inst._displayFormatted(date);
599
+ }
600
+ }
601
+ });
544
602
  _localeObserver();
545
603
  })();
@@ -0,0 +1,58 @@
1
+ # ln-debug
2
+
3
+ > The diagnostic component of `ln-ashlar` that enables verbose developer warnings in console logs during development.
4
+
5
+ ---
6
+
7
+ ## 1. Philosophy
8
+
9
+ In production, libraries should remain silent and not pollute the browser console with developer-oriented warnings (such as template mismatch warnings or missing configuration parameters).
10
+
11
+ The `ln-debug` component solves this by acting as a global switch:
12
+ - When active, all core validation warnings (prefixed with `[ln-` or `[lnCore`) are enabled.
13
+ - When inactive, these developer warnings are safely suppressed, maintaining a clean production log.
14
+
15
+ ---
16
+
17
+ ## 2. Minimal Blueprint
18
+
19
+ Add the `data-ln-debug` attribute to either the `<html>` or `<body>` element (or any element in the document) to enable debug logs:
20
+
21
+ ```html
22
+ <!DOCTYPE html>
23
+ <html lang="en" data-ln-debug>
24
+ <head>
25
+ <script src="dist/ln-ashlar.iife.js" defer></script>
26
+ </head>
27
+ <body>
28
+ <!-- Warnings will be visible in the developer console -->
29
+ </body>
30
+ </html>
31
+ ```
32
+
33
+ ---
34
+
35
+ ## 3. DOM & State Management
36
+
37
+ This component lives entirely at the level of the DOM. The single source of truth is the presence of the `data-ln-debug` attribute on `<html>` or `<body>`.
38
+
39
+ ### Checking the State via JS
40
+
41
+ To check if debug mode is active in your scripts, query the DOM directly:
42
+
43
+ ```javascript
44
+ const isDebug = document.documentElement.hasAttribute('data-ln-debug') ||
45
+ (document.body && document.body.hasAttribute('data-ln-debug'));
46
+ ```
47
+
48
+ ### Toggling the State
49
+
50
+ To toggle debug mode at runtime, add or remove the attribute from the DOM:
51
+
52
+ ```javascript
53
+ // Enable
54
+ document.documentElement.setAttribute('data-ln-debug', '');
55
+
56
+ // Disable
57
+ document.documentElement.removeAttribute('data-ln-debug');
58
+ ```