@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,151 @@
1
+ # ln-date
2
+
3
+ Locale-aware date formatting with native browser picker.
4
+
5
+ ## Usage
6
+
7
+ ```html
8
+ <input type="date" name="birthday" data-ln-date>
9
+ ```
10
+
11
+ On initialization, the component transforms the input into a text-display element with a paired calendar button and a hidden form-submit input. See [`docs/js/date.md`](../../docs/js/date.md) for the full transform.
12
+
13
+ ## Typing Support
14
+
15
+ The visible input accepts typed dates. On blur, the component parses the
16
+ input and reformats it according to the configured display format.
17
+
18
+ ### Supported Input Formats
19
+
20
+ | Separator | Assumed Format | Example |
21
+ |-----------|---------------|---------|
22
+ | `.` (dot) | dd.MM.yyyy (European) | `30.12.1979` or `30.12.79` |
23
+ | `/` (slash) | MM/dd/yyyy (US) | `12/30/1979` or `12/30/79` |
24
+ | `-` (dash) | yyyy-MM-dd if 4-digit first part (ISO), else dd-MM-yyyy | `1979-12-30` or `30-12-1979` |
25
+
26
+ Two-digit years: 00–49 → 2000–2049, 50–99 → 1950–1999.
27
+
28
+ ### Blur Behavior
29
+
30
+ - **Empty input**: clears the date value and dispatches `ln-date:change`
31
+ - **Unchanged text**: no action (user clicked in and out without editing)
32
+ - **Valid date**: updates the value, reformats display, dispatches `ln-date:change`
33
+ - **Invalid input**: reverts to the previous formatted display
34
+
35
+ The hidden input holds the ISO date string (YYYY-MM-DD) for form submission.
36
+
37
+ ## Attributes
38
+
39
+ | Attribute | On | Description |
40
+ |-----------|-----|-------------|
41
+ | `data-ln-date` | `<input>` | Enables date formatting. Value = format (keyword or pattern). Default: `medium` |
42
+
43
+ ### Format Keywords (locale-aware via Intl.DateTimeFormat)
44
+
45
+ | Value | Example (mk) | Example (en) |
46
+ |-------|-------------|-------------|
47
+ | (empty) | 19 апр 2026 | Apr 19, 2026 |
48
+ | `short` | 19.4.2026 | 4/19/2026 |
49
+ | `medium` | 19 апр 2026 | Apr 19, 2026 |
50
+ | `medium datetime` | 19 апр 2026, 14:30 | Apr 19, 2026, 2:30 PM |
51
+ | `long` | 19 април 2026 | April 19, 2026 |
52
+ | `short datetime` | 19.4.2026 14:30 | 4/19/26, 2:30 PM |
53
+ | `long datetime` | 19 април 2026, 14:30 | April 19, 2026, 2:30 PM |
54
+
55
+ ### Custom Pattern Tokens
56
+
57
+ | Token | Meaning | Example |
58
+ |-------|---------|---------|
59
+ | `dd` | Day 2-digit | 05 |
60
+ | `d` | Day | 5 |
61
+ | `MM` | Month 2-digit | 04 |
62
+ | `M` | Month | 4 |
63
+ | `MMM` | Month short name (locale) | апр |
64
+ | `MMMM` | Month full name (locale) | април |
65
+ | `yyyy` | Year 4-digit | 2026 |
66
+ | `yy` | Year 2-digit | 26 |
67
+ | `HH` | Hours 24h | 14 |
68
+ | `mm` | Minutes | 30 |
69
+
70
+ ## Events
71
+
72
+ | Event | Bubbles | Cancelable | Detail |
73
+ |-------|---------|------------|--------|
74
+ | `ln-date:change` | yes | no | `{ value: String (ISO), formatted: String, date: Date }` |
75
+ | `ln-date:destroyed` | yes | no | `{ target: Element }` |
76
+
77
+ ## API
78
+
79
+ ```javascript
80
+ const el = document.querySelector('[data-ln-date]');
81
+
82
+ el.lnDate.value; // get ISO date string (YYYY-MM-DD) or empty string
83
+ el.lnDate.value = '2026-04-19'; // set value programmatically — formats display
84
+ el.lnDate.date; // get Date object or null
85
+ el.lnDate.date = new Date(); // set via Date object
86
+ el.lnDate.formatted; // get formatted display string
87
+
88
+ el.lnDate.destroy(); // remove component, restore original input
89
+ ```
90
+
91
+ ## Locale Detection
92
+
93
+ Locale is detected by walking up the DOM tree to find the nearest `[lang]` attribute (e.g. `<html lang="mk">` → `mk`, `<html lang="en-US">` → `en-US`). Fallback: `navigator.language`. Changes to the `lang` attribute on `<html>` automatically re-format all instances.
94
+
95
+ ## Examples
96
+
97
+ ```html
98
+ <!-- Default (medium) -->
99
+ <div class="form-element">
100
+ <label for="birthday">Birthday</label>
101
+ <input type="date" id="birthday" name="birthday" data-ln-date>
102
+ </div>
103
+
104
+ <!-- Short format -->
105
+ <div class="form-element">
106
+ <label for="due">Due Date</label>
107
+ <input type="date" id="due" name="due" data-ln-date="short">
108
+ </div>
109
+
110
+ <!-- Long format -->
111
+ <div class="form-element">
112
+ <label for="event">Event Date</label>
113
+ <input type="date" id="event" name="event" data-ln-date="long">
114
+ </div>
115
+
116
+ <!-- Custom pattern -->
117
+ <div class="form-element">
118
+ <label for="start">Start Date</label>
119
+ <input type="date" id="start" name="start" data-ln-date="dd.MM.yyyy">
120
+ </div>
121
+
122
+ <!-- Pre-filled value -->
123
+ <div class="form-element">
124
+ <label for="hired">Hire Date</label>
125
+ <input type="date" id="hired" name="hired" value="2024-03-15" data-ln-date>
126
+ </div>
127
+ ```
128
+
129
+ ## Integration with ln-form
130
+
131
+ Works automatically. The hidden input carries the `name` attribute, so form serialization and population go through it transparently.
132
+
133
+ ## Integration and Source Files
134
+
135
+ This component can be loaded either as part of the unified Ashlar bundle or as a standalone zero-dependency script.
136
+
137
+ ### 1. In-Bundle (Standard Integration)
138
+ Include the main Ashlar bundle in your HTML. This bundle registers all Ashlar components, including `ln-date`:
139
+ ```html
140
+ <script src="dist/ln-ashlar.iife.js" defer></script>
141
+ ```
142
+
143
+ ### 2. Standalone (Zero-Dependency IIFE)
144
+ If you only need date-formatting functionality without the rest of the Ashlar library, load the component's standalone IIFE script:
145
+ ```html
146
+ <script src="js/ln-date/ln-date.js" defer></script>
147
+ ```
148
+
149
+ ### 3. Source & Reference
150
+ * **Active Development Source**: [js/ln-date/src/ln-date.js](file:///c:/laragon/www/ln-ashlar/js/ln-date/src/ln-date.js) — The source of truth containing the ES module implementation.
151
+ * **Compiled Standalone Release**: [js/ln-date/ln-date.js](file:///c:/laragon/www/ln-ashlar/js/ln-date/ln-date.js) — The compiled IIFE distribution file.
@@ -0,0 +1 @@
1
+ (function(){"use strict";function b(s,i,p){s.dispatchEvent(new CustomEvent(i,{bubbles:!0,detail:p||{}}))}function C(s,i){if(!document.body){document.addEventListener("DOMContentLoaded",function(){C(s,i)}),console.warn("["+i+'] Script loaded before <body> — add "defer" to your <script> tag');return}s()}function I(s,i,p,u){if(s.nodeType!==1)return;const M=i.indexOf("[")!==-1||i.indexOf(".")!==-1||i.indexOf("#")!==-1?i:"["+i+"]",S=Array.from(s.querySelectorAll(M));s.matches&&s.matches(M)&&S.push(s);for(const g of S)g[p]||(g[p]=new u(g))}function N(s){const i=s.closest("[lang]");return(i?i.lang:null)||navigator.language}function L(s,i,p,u,_={}){const M=_.extraAttributes||[],S=_.onAttributeChange||null,g=_.onInit||null;function w(O){const f=O||document.body;I(f,s,i,p),g&&g(f)}return C(function(){const O=new MutationObserver(function(h){for(let v=0;v<h.length;v++){const y=h[v];if(y.type==="childList")for(let t=0;t<y.addedNodes.length;t++){const e=y.addedNodes[t];e.nodeType===1&&(I(e,s,i,p),g&&g(e))}else y.type==="attributes"&&(S&&y.target[i]?S(y.target,y.attributeName):(I(y.target,s,i,p),g&&g(y.target)))}});let f=[];if(s.indexOf("[")!==-1){const h=/\[([\w-]+)/g;let v;for(;(v=h.exec(s))!==null;)f.push(v[1])}else f.push(s);O.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:f.concat(M)})},u),window[i]=w,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){w(document.body)}):w(document.body),w}const A={};function P(s,i){A[s]=i}function x(s){return A[s]||{ingress:i=>i,egress:i=>i}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=P,window.lnCore.getDataMapper=x),(function(){const s="data-ln-date",i="lnDate";if(window[i]!==void 0)return;const p={},u=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"value");function _(t,e){const n=t+"|"+JSON.stringify(e);return p[n]||(p[n]=new Intl.DateTimeFormat(t,e)),p[n]}const M=/^(short|medium|long)(\s+datetime)?$/,S={short:{dateStyle:"short"},medium:{dateStyle:"medium"},long:{dateStyle:"long"},"short datetime":{dateStyle:"short",timeStyle:"short"},"medium datetime":{dateStyle:"medium",timeStyle:"short"},"long datetime":{dateStyle:"long",timeStyle:"short"}};function g(t){return!t||t===""?{dateStyle:"medium"}:t.match(M)?S[t]:null}function w(t,e,n){const o=t.getDate(),d=t.getMonth(),a=t.getFullYear(),c=t.getHours(),r=t.getMinutes(),l={yyyy:String(a),yy:String(a).slice(-2),MMMM:_(n,{month:"long"}).format(t),MMM:_(n,{month:"short"}).format(t),MM:String(d+1).padStart(2,"0"),M:String(d+1),dd:String(o).padStart(2,"0"),d:String(o),HH:String(c).padStart(2,"0"),mm:String(r).padStart(2,"0")};return e.replace(/yyyy|yy|MMMM|MMM|MM|M|dd|d|HH|mm/g,function(m){return l[m]})}function O(t,e,n){const o=g(e);return o?_(n,o).format(t):w(t,e,n)}function f(t){if(t.tagName!=="INPUT")return console.warn("[ln-date] Can only be applied to <input>, got:",t.tagName),this;this.dom=t;const e=this,n=t.value,o=t.name,d=document.createElement("input");d.type="hidden",d.name=o,t.removeAttribute("name"),t.insertAdjacentElement("afterend",d),this._hidden=d;const a=document.createElement("input");a.type="date",a.tabIndex=-1,a.style.cssText="position:absolute;opacity:0;width:0;height:0;overflow:hidden;pointer-events:none",d.insertAdjacentElement("afterend",a),this._picker=a,t.type="text";const c=document.createElement("button");if(c.type="button",c.setAttribute("aria-label","Open date picker"),c.innerHTML='<svg class="ln-icon" aria-hidden="true"><use href="#ln-calendar"></use></svg>',a.insertAdjacentElement("afterend",c),this._btn=c,this._lastISO="",Object.defineProperty(d,"value",{get:function(){return u.get.call(d)},set:function(r){if(u.set.call(d,r),r&&r!==""){const l=h(r);l&&(e._displayFormatted(l),u.set.call(a,r))}else r===""&&(e.dom.value="",u.set.call(a,""))}}),this._onPickerChange=function(){const r=a.value;if(r){const l=h(r);l&&(e._setHiddenRaw(r),e._displayFormatted(l),e._lastISO=r,b(e.dom,"ln-date:change",{value:r,formatted:e.dom.value,date:l}))}else e._setHiddenRaw(""),e.dom.value="",e._lastISO="",b(e.dom,"ln-date:change",{value:"",formatted:"",date:null})},a.addEventListener("change",this._onPickerChange),this._onBlur=function(){const r=e.dom.value.trim();if(r===""){e._lastISO!==""&&(e._setHiddenRaw(""),u.set.call(e._picker,""),e.dom.value="",e._lastISO="",b(e.dom,"ln-date:change",{value:"",formatted:"",date:null}));return}if(e._lastISO){const m=h(e._lastISO);if(m){const E=e.dom.getAttribute(s)||"",D=N(e.dom),k=O(m,E,D);if(r===k)return}}const l=v(r);if(l){const m=l.getFullYear(),E=String(l.getMonth()+1).padStart(2,"0"),D=String(l.getDate()).padStart(2,"0"),k=m+"-"+E+"-"+D;e._setHiddenRaw(k),u.set.call(e._picker,k),e._displayFormatted(l),e._lastISO=k,b(e.dom,"ln-date:change",{value:k,formatted:e.dom.value,date:l})}else if(e._lastISO){const m=h(e._lastISO);m&&e._displayFormatted(m)}else e.dom.value=""},t.addEventListener("blur",this._onBlur),this._onBtnClick=function(){e._openPicker()},c.addEventListener("click",this._onBtnClick),n&&n!==""){const r=h(n);r&&(this._setHiddenRaw(n),u.set.call(a,n),this._displayFormatted(r),this._lastISO=n)}return this}function h(t){if(!t||typeof t!="string")return null;const e=t.split("T"),n=e[0].split("-");if(n.length<3)return null;const o=parseInt(n[0],10),d=parseInt(n[1],10)-1,a=parseInt(n[2],10);if(isNaN(o)||isNaN(d)||isNaN(a))return null;let c=0,r=0;if(e[1]){const m=e[1].split(":");c=parseInt(m[0],10)||0,r=parseInt(m[1],10)||0}const l=new Date(o,d,a,c,r);return l.getFullYear()!==o||l.getMonth()!==d||l.getDate()!==a?null:l}function v(t){if(!t||typeof t!="string"||(t=t.trim(),t.length<6))return null;let e,n;if(t.indexOf(".")!==-1)e=".",n=t.split(".");else if(t.indexOf("/")!==-1)e="/",n=t.split("/");else if(t.indexOf("-")!==-1)e="-",n=t.split("-");else return null;if(n.length!==3)return null;const o=[];for(let l=0;l<3;l++){const m=parseInt(n[l],10);if(isNaN(m))return null;o.push(m)}let d,a,c;e==="."?(d=o[0],a=o[1],c=o[2]):e==="/"?(a=o[0],d=o[1],c=o[2]):n[0].length===4?(c=o[0],a=o[1],d=o[2]):(d=o[0],a=o[1],c=o[2]),c<100&&(c+=c<50?2e3:1900);const r=new Date(c,a-1,d);return r.getFullYear()!==c||r.getMonth()!==a-1||r.getDate()!==d?null:r}f.prototype._openPicker=function(){if(typeof this._picker.showPicker=="function")try{this._picker.showPicker()}catch{this._picker.click()}else this._picker.click()},f.prototype._setHiddenRaw=function(t){u.set.call(this._hidden,t)},f.prototype._displayFormatted=function(t){const e=this.dom.getAttribute(s)||"",n=N(this.dom);this.dom.value=O(t,e,n)},Object.defineProperty(f.prototype,"value",{get:function(){return u.get.call(this._hidden)},set:function(t){if(!t||t===""){this._setHiddenRaw(""),u.set.call(this._picker,""),this.dom.value="",this._lastISO="";return}const e=h(t);e&&(this._setHiddenRaw(t),u.set.call(this._picker,t),this._displayFormatted(e),this._lastISO=t,b(this.dom,"ln-date:change",{value:t,formatted:this.dom.value,date:e}))}}),Object.defineProperty(f.prototype,"date",{get:function(){const t=this.value;return t?h(t):null},set:function(t){if(!t||!(t instanceof Date)||isNaN(t.getTime())){this.value="";return}const e=t.getFullYear(),n=String(t.getMonth()+1).padStart(2,"0"),o=String(t.getDate()).padStart(2,"0");this.value=e+"-"+n+"-"+o}}),Object.defineProperty(f.prototype,"formatted",{get:function(){return this.dom.value}}),f.prototype.destroy=function(){if(!this.dom[i])return;this._picker.removeEventListener("change",this._onPickerChange),this.dom.removeEventListener("blur",this._onBlur),this._btn.removeEventListener("click",this._onBtnClick),this.dom.name=this._hidden.name,this.dom.type="date";const t=this.value;this._hidden.remove(),this._picker.remove(),this._btn.remove(),t&&(this.dom.value=t),b(this.dom,"ln-date:destroyed",{target:this.dom}),delete this.dom[i]};function y(){new MutationObserver(function(){const t=document.querySelectorAll("["+s+"]");for(let e=0;e<t.length;e++){const n=t[e][i];if(n&&n.value){const o=h(n.value);o&&n._displayFormatted(o)}}}).observe(document.documentElement,{attributes:!0,attributeFilter:["lang"]})}L(s,i,f,"ln-date"),y()})()})();
@@ -0,0 +1,442 @@
1
+ import { dispatch, getLocale, registerComponent } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-date';
5
+ const DOM_ATTRIBUTE = 'lnDate';
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, options) {
15
+ const key = locale + '|' + JSON.stringify(options);
16
+ if (!_formatters[key]) {
17
+ _formatters[key] = new Intl.DateTimeFormat(locale, options);
18
+ }
19
+ return _formatters[key];
20
+ }
21
+
22
+ // ─── Format Detection ─────────────────────────────────────
23
+
24
+ const KEYWORD_RE = /^(short|medium|long)(\s+datetime)?$/;
25
+
26
+ const KEYWORD_OPTIONS = {
27
+ 'short': { dateStyle: 'short' },
28
+ 'medium': { dateStyle: 'medium' },
29
+ 'long': { dateStyle: 'long' },
30
+ 'short datetime': { dateStyle: 'short', timeStyle: 'short' },
31
+ 'medium datetime': { dateStyle: 'medium', timeStyle: 'short' },
32
+ 'long datetime': { dateStyle: 'long', timeStyle: 'short' }
33
+ };
34
+
35
+ function _getIntlOptions(format) {
36
+ if (!format || format === '') return { dateStyle: 'medium' };
37
+ const match = format.match(KEYWORD_RE);
38
+ if (match) {
39
+ return KEYWORD_OPTIONS[format];
40
+ }
41
+ return null; // custom pattern
42
+ }
43
+
44
+ // ─── Custom Pattern Formatting ────────────────────────────
45
+
46
+ function _formatCustom(date, pattern, locale) {
47
+ const day = date.getDate();
48
+ const month = date.getMonth();
49
+ const year = date.getFullYear();
50
+ const hours = date.getHours();
51
+ const minutes = date.getMinutes();
52
+
53
+ const tokens = {
54
+ 'yyyy': String(year),
55
+ 'yy': String(year).slice(-2),
56
+ 'MMMM': _getFormatter(locale, { month: 'long' }).format(date),
57
+ 'MMM': _getFormatter(locale, { month: 'short' }).format(date),
58
+ 'MM': String(month + 1).padStart(2, '0'),
59
+ 'M': String(month + 1),
60
+ 'dd': String(day).padStart(2, '0'),
61
+ 'd': String(day),
62
+ 'HH': String(hours).padStart(2, '0'),
63
+ 'mm': String(minutes).padStart(2, '0')
64
+ };
65
+
66
+ return pattern.replace(/yyyy|yy|MMMM|MMM|MM|M|dd|d|HH|mm/g, function (m) { return tokens[m]; });
67
+ }
68
+
69
+ // ─── Format Date ──────────────────────────────────────────
70
+
71
+ function _formatDate(date, format, locale) {
72
+ const intlOptions = _getIntlOptions(format);
73
+ if (intlOptions) {
74
+ return _getFormatter(locale, intlOptions).format(date);
75
+ }
76
+ return _formatCustom(date, format, locale);
77
+ }
78
+
79
+ // ─── Component ────────────────────────────────────────────
80
+
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;
85
+ }
86
+
87
+ this.dom = dom;
88
+ const self = this;
89
+
90
+ // ── Read initial state ──────────────────────────────
91
+ const initialValue = dom.value; // ISO string (YYYY-MM-DD) or empty
92
+ const name = dom.name;
93
+
94
+ // ── Create hidden input for form submission ─────────
95
+ const hidden = document.createElement('input');
96
+ hidden.type = 'hidden';
97
+ hidden.name = name;
98
+ dom.removeAttribute('name');
99
+ dom.insertAdjacentElement('afterend', hidden);
100
+ this._hidden = hidden;
101
+
102
+ // ── Create hidden date input for native picker ──────
103
+ const picker = document.createElement('input');
104
+ picker.type = 'date';
105
+ picker.tabIndex = -1;
106
+ picker.style.cssText = 'position:absolute;opacity:0;width:0;height:0;overflow:hidden;pointer-events:none';
107
+ hidden.insertAdjacentElement('afterend', picker);
108
+ this._picker = picker;
109
+
110
+ // ── Transform original input to text display ────────
111
+ dom.type = 'text';
112
+
113
+ // ── Create calendar button ──────────────────────────
114
+ const btn = document.createElement('button');
115
+ btn.type = 'button';
116
+ btn.setAttribute('aria-label', 'Open date picker');
117
+ btn.innerHTML = '<svg class="ln-icon" aria-hidden="true"><use href="#ln-calendar"></use></svg>';
118
+ picker.insertAdjacentElement('afterend', btn);
119
+ this._btn = btn;
120
+ this._lastISO = '';
121
+
122
+ // ── Intercept programmatic value sets on hidden input
123
+ Object.defineProperty(hidden, 'value', {
124
+ get: function () {
125
+ return _inputValueDesc.get.call(hidden);
126
+ },
127
+ set: function (val) {
128
+ _inputValueDesc.set.call(hidden, val);
129
+ if (val && val !== '') {
130
+ const date = _parseISO(val);
131
+ if (date) {
132
+ self._displayFormatted(date);
133
+ _inputValueDesc.set.call(picker, val);
134
+ }
135
+ } else if (val === '') {
136
+ self.dom.value = '';
137
+ _inputValueDesc.set.call(picker, '');
138
+ }
139
+ }
140
+ });
141
+
142
+ // ── Bind events ─────────────────────────────────────
143
+ this._onPickerChange = function () {
144
+ const val = picker.value; // ISO YYYY-MM-DD
145
+ if (val) {
146
+ const date = _parseISO(val);
147
+ if (date) {
148
+ self._setHiddenRaw(val);
149
+ self._displayFormatted(date);
150
+ self._lastISO = val;
151
+ dispatch(self.dom, 'ln-date:change', {
152
+ value: val,
153
+ formatted: self.dom.value,
154
+ date: date
155
+ });
156
+ }
157
+ } else {
158
+ self._setHiddenRaw('');
159
+ self.dom.value = '';
160
+ self._lastISO = '';
161
+ dispatch(self.dom, 'ln-date:change', {
162
+ value: '',
163
+ formatted: '',
164
+ date: null
165
+ });
166
+ }
167
+ };
168
+ picker.addEventListener('change', this._onPickerChange);
169
+
170
+ this._onBlur = function () {
171
+ const typed = self.dom.value.trim();
172
+
173
+ // Empty input — clear if there was a value
174
+ if (typed === '') {
175
+ if (self._lastISO !== '') {
176
+ self._setHiddenRaw('');
177
+ _inputValueDesc.set.call(self._picker, '');
178
+ self.dom.value = '';
179
+ self._lastISO = '';
180
+ dispatch(self.dom, 'ln-date:change', {
181
+ value: '',
182
+ formatted: '',
183
+ date: null
184
+ });
185
+ }
186
+ return;
187
+ }
188
+
189
+ // Check if text is unchanged from current formatted display
190
+ if (self._lastISO) {
191
+ const currentDate = _parseISO(self._lastISO);
192
+ if (currentDate) {
193
+ const format = self.dom.getAttribute(DOM_SELECTOR) || '';
194
+ const locale = getLocale(self.dom);
195
+ const currentFormatted = _formatDate(currentDate, format, locale);
196
+ if (typed === currentFormatted) return;
197
+ }
198
+ }
199
+
200
+ // Try to parse the typed value
201
+ const parsed = _parseTyped(typed);
202
+ if (parsed) {
203
+ const y = parsed.getFullYear();
204
+ const m = String(parsed.getMonth() + 1).padStart(2, '0');
205
+ const d = String(parsed.getDate()).padStart(2, '0');
206
+ const iso = y + '-' + m + '-' + d;
207
+ self._setHiddenRaw(iso);
208
+ _inputValueDesc.set.call(self._picker, iso);
209
+ self._displayFormatted(parsed);
210
+ self._lastISO = iso;
211
+ dispatch(self.dom, 'ln-date:change', {
212
+ value: iso,
213
+ formatted: self.dom.value,
214
+ date: parsed
215
+ });
216
+ } else {
217
+ // Invalid input — revert to previous display
218
+ if (self._lastISO) {
219
+ const prevDate = _parseISO(self._lastISO);
220
+ if (prevDate) self._displayFormatted(prevDate);
221
+ } else {
222
+ self.dom.value = '';
223
+ }
224
+ }
225
+ };
226
+ dom.addEventListener('blur', this._onBlur);
227
+
228
+ this._onBtnClick = function () {
229
+ self._openPicker();
230
+ };
231
+ btn.addEventListener('click', this._onBtnClick);
232
+
233
+ // ── Handle pre-filled value ─────────────────────────
234
+ if (initialValue && initialValue !== '') {
235
+ const date = _parseISO(initialValue);
236
+ if (date) {
237
+ this._setHiddenRaw(initialValue);
238
+ _inputValueDesc.set.call(picker, initialValue);
239
+ this._displayFormatted(date);
240
+ this._lastISO = initialValue;
241
+ }
242
+ }
243
+
244
+ return this;
245
+ }
246
+
247
+ // ─── Helpers ──────────────────────────────────────────────
248
+
249
+ function _parseISO(str) {
250
+ // Parse YYYY-MM-DD (and optionally YYYY-MM-DDTHH:mm)
251
+ if (!str || typeof str !== 'string') return null;
252
+ const parts = str.split('T');
253
+ const dateParts = parts[0].split('-');
254
+ if (dateParts.length < 3) return null;
255
+ const y = parseInt(dateParts[0], 10);
256
+ const m = parseInt(dateParts[1], 10) - 1;
257
+ const d = parseInt(dateParts[2], 10);
258
+ if (isNaN(y) || isNaN(m) || isNaN(d)) return null;
259
+ let h = 0, min = 0;
260
+ if (parts[1]) {
261
+ const timeParts = parts[1].split(':');
262
+ h = parseInt(timeParts[0], 10) || 0;
263
+ min = parseInt(timeParts[1], 10) || 0;
264
+ }
265
+ const date = new Date(y, m, d, h, min);
266
+ // Validate the date is real (e.g., Feb 30 would roll over)
267
+ if (date.getFullYear() !== y || date.getMonth() !== m || date.getDate() !== d) return null;
268
+ return date;
269
+ }
270
+
271
+ function _parseTyped(str) {
272
+ if (!str || typeof str !== 'string') return null;
273
+ str = str.trim();
274
+ if (str.length < 6) return null; // minimum: d.M.yy
275
+
276
+ // Detect separator
277
+ let sep, parts;
278
+ if (str.indexOf('.') !== -1) {
279
+ sep = '.';
280
+ parts = str.split('.');
281
+ } else if (str.indexOf('/') !== -1) {
282
+ sep = '/';
283
+ parts = str.split('/');
284
+ } else if (str.indexOf('-') !== -1) {
285
+ sep = '-';
286
+ parts = str.split('-');
287
+ } else {
288
+ return null;
289
+ }
290
+
291
+ if (parts.length !== 3) return null;
292
+ const nums = [];
293
+ for (let i = 0; i < 3; i++) {
294
+ const n = parseInt(parts[i], 10);
295
+ if (isNaN(n)) return null;
296
+ nums.push(n);
297
+ }
298
+
299
+ let day, month, year;
300
+
301
+ if (sep === '.') {
302
+ // dd.MM.yyyy (European)
303
+ day = nums[0]; month = nums[1]; year = nums[2];
304
+ } else if (sep === '/') {
305
+ // MM/dd/yyyy (US)
306
+ month = nums[0]; day = nums[1]; year = nums[2];
307
+ } else {
308
+ // dash: yyyy-MM-dd if first part is 4 digits, else dd-MM-yyyy
309
+ if (parts[0].length === 4) {
310
+ year = nums[0]; month = nums[1]; day = nums[2];
311
+ } else {
312
+ day = nums[0]; month = nums[1]; year = nums[2];
313
+ }
314
+ }
315
+
316
+ // Two-digit year pivot
317
+ if (year < 100) {
318
+ year += (year < 50) ? 2000 : 1900;
319
+ }
320
+
321
+ // Validate via Date constructor roundtrip
322
+ const date = new Date(year, month - 1, day);
323
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
324
+ return null;
325
+ }
326
+ return date;
327
+ }
328
+
329
+ _component.prototype._openPicker = function () {
330
+ if (typeof this._picker.showPicker === 'function') {
331
+ try {
332
+ this._picker.showPicker();
333
+ } catch (e) {
334
+ // showPicker() can throw if not triggered by user gesture
335
+ this._picker.click();
336
+ }
337
+ } else {
338
+ this._picker.click();
339
+ }
340
+ };
341
+
342
+ _component.prototype._setHiddenRaw = function (val) {
343
+ _inputValueDesc.set.call(this._hidden, val);
344
+ };
345
+
346
+ _component.prototype._displayFormatted = function (date) {
347
+ const format = this.dom.getAttribute(DOM_SELECTOR) || '';
348
+ const locale = getLocale(this.dom);
349
+ this.dom.value = _formatDate(date, format, locale);
350
+ };
351
+
352
+ // ─── Public API ───────────────────────────────────────────
353
+
354
+ Object.defineProperty(_component.prototype, 'value', {
355
+ get: function () {
356
+ return _inputValueDesc.get.call(this._hidden);
357
+ },
358
+ set: function (isoStr) {
359
+ if (!isoStr || isoStr === '') {
360
+ this._setHiddenRaw('');
361
+ _inputValueDesc.set.call(this._picker, '');
362
+ this.dom.value = '';
363
+ this._lastISO = '';
364
+ return;
365
+ }
366
+ const date = _parseISO(isoStr);
367
+ if (!date) return;
368
+ this._setHiddenRaw(isoStr);
369
+ _inputValueDesc.set.call(this._picker, isoStr);
370
+ this._displayFormatted(date);
371
+ this._lastISO = isoStr;
372
+ dispatch(this.dom, 'ln-date:change', {
373
+ value: isoStr,
374
+ formatted: this.dom.value,
375
+ date: date
376
+ });
377
+ }
378
+ });
379
+
380
+ Object.defineProperty(_component.prototype, 'date', {
381
+ get: function () {
382
+ const val = this.value;
383
+ if (!val) return null;
384
+ return _parseISO(val);
385
+ },
386
+ set: function (dateObj) {
387
+ if (!dateObj || !(dateObj instanceof Date) || isNaN(dateObj.getTime())) {
388
+ this.value = '';
389
+ return;
390
+ }
391
+ const y = dateObj.getFullYear();
392
+ const m = String(dateObj.getMonth() + 1).padStart(2, '0');
393
+ const d = String(dateObj.getDate()).padStart(2, '0');
394
+ this.value = y + '-' + m + '-' + d;
395
+ }
396
+ });
397
+
398
+ Object.defineProperty(_component.prototype, 'formatted', {
399
+ get: function () {
400
+ return this.dom.value;
401
+ }
402
+ });
403
+
404
+ _component.prototype.destroy = function () {
405
+ if (!this.dom[DOM_ATTRIBUTE]) return;
406
+ this._picker.removeEventListener('change', this._onPickerChange);
407
+ this.dom.removeEventListener('blur', this._onBlur);
408
+ this._btn.removeEventListener('click', this._onBtnClick);
409
+ // Restore original input
410
+ this.dom.name = this._hidden.name;
411
+ this.dom.type = 'date';
412
+ const isoVal = this.value;
413
+ // Remove created elements
414
+ this._hidden.remove();
415
+ this._picker.remove();
416
+ this._btn.remove();
417
+ // Restore value
418
+ if (isoVal) this.dom.value = isoVal;
419
+ dispatch(this.dom, 'ln-date:destroyed', { target: this.dom });
420
+ delete this.dom[DOM_ATTRIBUTE];
421
+ };
422
+
423
+ // ─── Locale Observer ──────────────────────────────────────
424
+
425
+ function _localeObserver() {
426
+ new MutationObserver(function () {
427
+ const els = document.querySelectorAll('[' + DOM_SELECTOR + ']');
428
+ for (let i = 0; i < els.length; i++) {
429
+ const inst = els[i][DOM_ATTRIBUTE];
430
+ if (inst && inst.value) {
431
+ const date = _parseISO(inst.value);
432
+ if (date) inst._displayFormatted(date);
433
+ }
434
+ }
435
+ }).observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] });
436
+ }
437
+
438
+ // ─── Init ─────────────────────────────────────────────────
439
+
440
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-date');
441
+ _localeObserver();
442
+ })();