@livenetworks/ashlar 1.6.1 → 2.0.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 (559) hide show
  1. package/LICENSE +641 -0
  2. package/README.md +202 -86
  3. package/{js → components}/COMPONENTS.md +1116 -1108
  4. package/{js → components}/index.js +81 -58
  5. package/{js → components}/ln-accordion/README.md +7 -6
  6. package/components/ln-accordion/ln-accordion-dev.scss +8 -0
  7. package/components/ln-accordion/ln-accordion.js +1 -0
  8. package/components/ln-accordion/ln-accordion.schema.json +30 -0
  9. package/components/ln-accordion/ln-accordion.scss +8 -0
  10. package/{js → components}/ln-accordion/src/ln-accordion.js +8 -2
  11. package/{js → components}/ln-ajax/README.md +171 -160
  12. package/components/ln-ajax/ln-ajax-dev.scss +8 -0
  13. package/components/ln-ajax/ln-ajax.js +1 -0
  14. package/components/ln-ajax/ln-ajax.schema.json +30 -0
  15. package/{js → components}/ln-ajax/src/ln-ajax.js +65 -30
  16. package/{js → components}/ln-api-connector/README.md +27 -5
  17. package/components/ln-api-connector/ln-api-connector.js +1 -0
  18. package/components/ln-api-connector/ln-api-connector.schema.json +96 -0
  19. package/components/ln-api-connector/src/connector-core.js +97 -0
  20. package/components/ln-api-connector/src/ln-api-connector.js +447 -0
  21. package/{js → components}/ln-api-queue/README.md +12 -9
  22. package/components/ln-api-queue/ln-api-queue.js +1 -0
  23. package/components/ln-api-queue/ln-api-queue.schema.json +34 -0
  24. package/{js → components}/ln-api-queue/src/ln-api-queue.js +33 -14
  25. package/{js → components}/ln-api-queue/src/queue-storage.js +7 -3
  26. package/{js → components}/ln-autoresize/README.md +129 -127
  27. package/components/ln-autoresize/ln-autoresize-dev.scss +8 -0
  28. package/components/ln-autoresize/ln-autoresize.js +1 -0
  29. package/components/ln-autoresize/ln-autoresize.schema.json +22 -0
  30. package/{js → components}/ln-autoresize/src/ln-autoresize.js +8 -1
  31. package/{js → components}/ln-autosave/README.md +127 -124
  32. package/components/ln-autosave/ln-autosave-dev.scss +13 -0
  33. package/components/ln-autosave/ln-autosave.js +1 -0
  34. package/components/ln-autosave/ln-autosave.schema.json +48 -0
  35. package/components/ln-autosave/src/autosave-model.js +34 -0
  36. package/{js → components}/ln-autosave/src/ln-autosave.js +65 -58
  37. package/components/ln-chart/README.md +145 -0
  38. package/components/ln-chart/ln-chart-dev.scss +20 -0
  39. package/components/ln-chart/ln-chart.js +1 -0
  40. package/components/ln-chart/ln-chart.schema.json +146 -0
  41. package/components/ln-chart/ln-chart.scss +8 -0
  42. package/components/ln-chart/src/chart-model.js +154 -0
  43. package/components/ln-chart/src/ln-chart.js +176 -0
  44. package/{js → components}/ln-circular-progress/README.md +15 -14
  45. package/components/ln-circular-progress/ln-circular-progress-dev.scss +8 -0
  46. package/components/ln-circular-progress/ln-circular-progress.js +1 -0
  47. package/components/ln-circular-progress/ln-circular-progress.schema.json +42 -0
  48. package/{js → components}/ln-circular-progress/src/ln-circular-progress.js +32 -50
  49. package/{js → components}/ln-confirm/README.md +69 -18
  50. package/components/ln-confirm/ln-confirm-dev.scss +24 -0
  51. package/components/ln-confirm/ln-confirm.js +1 -0
  52. package/components/ln-confirm/ln-confirm.schema.json +66 -0
  53. package/components/ln-confirm/ln-confirm.scss +24 -0
  54. package/{js → components}/ln-confirm/src/ln-confirm.js +180 -171
  55. package/{js → components}/ln-core/README.md +374 -149
  56. package/components/ln-core/attrs.js +273 -0
  57. package/components/ln-core/compare.js +40 -0
  58. package/components/ln-core/crypto.js +265 -0
  59. package/components/ln-core/date.js +50 -0
  60. package/components/ln-core/dom.js +115 -0
  61. package/components/ln-core/events.js +122 -0
  62. package/components/ln-core/form.js +136 -0
  63. package/{js → components}/ln-core/hash.js +149 -92
  64. package/components/ln-core/helpers.js +33 -0
  65. package/components/ln-core/http.js +48 -0
  66. package/components/ln-core/index.js +11 -0
  67. package/components/ln-core/lifecycle.js +403 -0
  68. package/components/ln-core/ln-core.schema.json +109 -0
  69. package/components/ln-core/locale.js +55 -0
  70. package/components/ln-core/matching.js +78 -0
  71. package/components/ln-core/number.js +125 -0
  72. package/components/ln-core/progress.js +30 -0
  73. package/components/ln-core/template.js +206 -0
  74. package/{js → components}/ln-core/window-cache.js +247 -213
  75. package/{js → components}/ln-couchdb-connector/README.md +7 -5
  76. package/components/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
  77. package/components/ln-couchdb-connector/ln-couchdb-connector.schema.json +50 -0
  78. package/{js → components}/ln-couchdb-connector/src/ln-couchdb-connector.js +29 -43
  79. package/{js → components}/ln-data-coordinator/README.md +109 -73
  80. package/components/ln-data-coordinator/ln-data-coordinator-dev.scss +8 -0
  81. package/components/ln-data-coordinator/ln-data-coordinator.js +1 -0
  82. package/components/ln-data-coordinator/ln-data-coordinator.schema.json +196 -0
  83. package/components/ln-data-coordinator/src/data-read-policy.js +41 -0
  84. package/{js → components}/ln-data-coordinator/src/ln-data-coordinator.js +411 -154
  85. package/{js → components}/ln-data-coordinator/src/mutation-receipts.js +0 -5
  86. package/{js → components}/ln-data-store/README.md +42 -17
  87. package/components/ln-data-store/ln-data-store-dev.scss +13 -0
  88. package/components/ln-data-store/ln-data-store.js +1 -0
  89. package/components/ln-data-store/ln-data-store.schema.json +84 -0
  90. package/components/ln-data-store/src/data-store-model.js +155 -0
  91. package/{js → components}/ln-data-store/src/ln-data-store.js +407 -105
  92. package/components/ln-data-store/src/window-index.js +119 -0
  93. package/{js → components}/ln-date/README.md +17 -7
  94. package/{js → components}/ln-date/ln-date-dev.scss +14 -14
  95. package/components/ln-date/ln-date.js +1 -0
  96. package/components/ln-date/ln-date.schema.json +92 -0
  97. package/components/ln-date/src/date-model.js +186 -0
  98. package/components/ln-date/src/ln-date.js +486 -0
  99. package/components/ln-debug/README.md +132 -0
  100. package/components/ln-debug/ln-debug.js +1 -0
  101. package/components/ln-debug/ln-debug.schema.json +28 -0
  102. package/components/ln-debug/src/console-sink.js +19 -0
  103. package/components/ln-debug/src/debug-verifier.js +331 -0
  104. package/components/ln-debug/src/gate.js +95 -0
  105. package/components/ln-debug/src/generated-attributes.js +299 -0
  106. package/components/ln-debug/src/ln-debug.js +60 -0
  107. package/{js → components}/ln-dropdown/README.md +150 -140
  108. package/components/ln-dropdown/ln-dropdown-dev.scss +13 -0
  109. package/components/ln-dropdown/ln-dropdown.js +1 -0
  110. package/components/ln-dropdown/ln-dropdown.schema.json +90 -0
  111. package/components/ln-dropdown/ln-dropdown.scss +18 -0
  112. package/{js → components}/ln-dropdown/src/ln-dropdown.js +114 -11
  113. package/{js → components}/ln-editor/README.md +11 -9
  114. package/components/ln-editor/ln-editor-dev.scss +18 -0
  115. package/components/ln-editor/ln-editor.js +1 -0
  116. package/components/ln-editor/ln-editor.schema.json +58 -0
  117. package/components/ln-editor/ln-editor.scss +13 -0
  118. package/{js → components}/ln-editor/src/ln-editor.js +59 -11
  119. package/{js → components}/ln-external-links/README.md +10 -8
  120. package/components/ln-external-links/ln-external-links.js +1 -0
  121. package/components/ln-external-links/ln-external-links.schema.json +18 -0
  122. package/{js → components}/ln-external-links/src/ln-external-links.js +13 -11
  123. package/{js → components}/ln-fill/README.md +6 -8
  124. package/components/ln-fill/ln-fill.js +1 -0
  125. package/components/ln-fill/ln-fill.schema.json +23 -0
  126. package/{js → components}/ln-fill/src/ln-fill.js +7 -1
  127. package/components/ln-filter/README.md +131 -0
  128. package/components/ln-filter/ln-filter-dev.scss +13 -0
  129. package/components/ln-filter/ln-filter.js +1 -0
  130. package/components/ln-filter/ln-filter.schema.json +153 -0
  131. package/components/ln-filter/src/filter-model.js +185 -0
  132. package/components/ln-filter/src/ln-filter.js +435 -0
  133. package/{js → components}/ln-form/README.md +260 -264
  134. package/components/ln-form/ln-form-dev.scss +8 -0
  135. package/components/ln-form/ln-form.js +1 -0
  136. package/components/ln-form/ln-form.schema.json +45 -0
  137. package/{js → components}/ln-form/src/ln-form.js +107 -99
  138. package/{js → components}/ln-http/README.md +11 -6
  139. package/components/ln-http/ln-http.js +1 -0
  140. package/components/ln-http/ln-http.schema.json +6 -0
  141. package/components/ln-http/src/http-core.js +51 -0
  142. package/{js → components}/ln-http/src/ln-http.js +28 -33
  143. package/{js → components}/ln-icon/README.md +16 -9
  144. package/components/ln-icon/ln-icon.schema.json +6 -0
  145. package/components/ln-include/README.md +75 -0
  146. package/components/ln-include/ln-include-dev.scss +13 -0
  147. package/components/ln-include/ln-include.js +1 -0
  148. package/components/ln-include/ln-include.schema.json +23 -0
  149. package/components/ln-include/src/ln-include.js +111 -0
  150. package/components/ln-key/README.md +130 -0
  151. package/components/ln-key/ln-key-dev.scss +18 -0
  152. package/components/ln-key/ln-key.js +1 -0
  153. package/components/ln-key/ln-key.schema.json +56 -0
  154. package/components/ln-key/src/key-model.js +146 -0
  155. package/components/ln-key/src/ln-key.js +201 -0
  156. package/{js → components}/ln-link/README.md +7 -9
  157. package/components/ln-link/ln-link-dev.scss +8 -0
  158. package/components/ln-link/ln-link.js +1 -0
  159. package/components/ln-link/ln-link.schema.json +22 -0
  160. package/{js → components}/ln-link/src/ln-link.js +16 -11
  161. package/components/ln-list/README.md +152 -0
  162. package/components/ln-list/ln-list-dev.scss +8 -0
  163. package/components/ln-list/ln-list.js +1 -0
  164. package/components/ln-list/ln-list.schema.json +166 -0
  165. package/components/ln-list/ln-list.scss +25 -0
  166. package/{js → components}/ln-list/src/ln-list.js +1301 -1216
  167. package/{js → components}/ln-modal/README.md +13 -10
  168. package/components/ln-modal/ln-modal-dev.scss +18 -0
  169. package/components/ln-modal/ln-modal.js +1 -0
  170. package/components/ln-modal/ln-modal.schema.json +54 -0
  171. package/components/ln-modal/ln-modal.scss +51 -0
  172. package/{js → components}/ln-modal/src/ln-modal.js +164 -137
  173. package/{js → components}/ln-nav/README.md +11 -11
  174. package/components/ln-nav/ln-nav.js +1 -0
  175. package/components/ln-nav/ln-nav.schema.json +25 -0
  176. package/{js → components}/ln-nav/src/ln-nav.js +25 -14
  177. package/components/ln-number/README.md +205 -0
  178. package/{js → components}/ln-number/ln-number-dev.scss +13 -13
  179. package/components/ln-number/ln-number.js +1 -0
  180. package/components/ln-number/ln-number.schema.json +63 -0
  181. package/components/ln-number/src/ln-number.js +400 -0
  182. package/components/ln-number/src/number-model.js +31 -0
  183. package/components/ln-obfuscator/README.md +60 -0
  184. package/components/ln-obfuscator/ln-obfuscator.js +1 -0
  185. package/components/ln-obfuscator/ln-obfuscator.schema.json +49 -0
  186. package/components/ln-obfuscator/src/ln-obfuscator.js +159 -0
  187. package/components/ln-obfuscator/src/obfuscator-model.js +250 -0
  188. package/{js → components}/ln-options/README.md +3 -1
  189. package/components/ln-options/ln-options-dev.scss +13 -0
  190. package/components/ln-options/ln-options.js +1 -0
  191. package/components/ln-options/ln-options.schema.json +41 -0
  192. package/{js → components}/ln-options/src/ln-options.js +13 -5
  193. package/components/ln-persist/README.md +151 -0
  194. package/components/ln-persist/ln-persist.js +1 -0
  195. package/components/ln-persist/ln-persist.schema.json +28 -0
  196. package/components/ln-persist/src/ln-persist.js +83 -0
  197. package/components/ln-picklist/README.md +230 -0
  198. package/components/ln-picklist/ln-picklist-dev.scss +22 -0
  199. package/components/ln-picklist/ln-picklist.js +1 -0
  200. package/components/ln-picklist/ln-picklist.schema.json +49 -0
  201. package/components/ln-picklist/src/ln-picklist.js +228 -0
  202. package/{js → components}/ln-popover/README.md +164 -162
  203. package/components/ln-popover/ln-popover-dev.scss +13 -0
  204. package/components/ln-popover/ln-popover.js +1 -0
  205. package/components/ln-popover/ln-popover.schema.json +82 -0
  206. package/components/ln-popover/ln-popover.scss +18 -0
  207. package/{js → components}/ln-popover/src/ln-popover.js +42 -36
  208. package/{js → components}/ln-progress/README.md +410 -413
  209. package/components/ln-progress/ln-progress-dev.scss +8 -0
  210. package/components/ln-progress/ln-progress.js +1 -0
  211. package/components/ln-progress/ln-progress.schema.json +40 -0
  212. package/components/ln-progress/src/ln-progress.js +91 -0
  213. package/components/ln-progress/src/progress-model.js +20 -0
  214. package/{js → components}/ln-router/README.md +443 -432
  215. package/components/ln-router/ln-router-dev.scss +13 -0
  216. package/components/ln-router/ln-router.js +1 -0
  217. package/components/ln-router/ln-router.schema.json +68 -0
  218. package/{js → components}/ln-router/src/ln-router.js +585 -539
  219. package/components/ln-router/src/router-model.js +50 -0
  220. package/components/ln-scroll/README.md +45 -0
  221. package/components/ln-scroll/ln-scroll.js +1 -0
  222. package/components/ln-scroll/ln-scroll.schema.json +85 -0
  223. package/components/ln-scroll/src/ln-scroll.js +150 -0
  224. package/components/ln-search/README.md +188 -0
  225. package/components/ln-search/ln-search-dev.scss +13 -0
  226. package/components/ln-search/ln-search.js +1 -0
  227. package/components/ln-search/ln-search.schema.json +103 -0
  228. package/components/ln-search/src/ln-search.js +330 -0
  229. package/{js → components}/ln-slug/README.md +6 -3
  230. package/components/ln-slug/ln-slug-dev.scss +18 -0
  231. package/components/ln-slug/ln-slug.js +1 -0
  232. package/components/ln-slug/ln-slug.schema.json +23 -0
  233. package/{js → components}/ln-slug/src/ln-slug.js +20 -13
  234. package/components/ln-slug/src/slug-model.js +21 -0
  235. package/{js → components}/ln-sort/README.md +178 -170
  236. package/{js → components}/ln-sort/ln-sort-dev.scss +8 -8
  237. package/components/ln-sort/ln-sort.js +1 -0
  238. package/components/ln-sort/ln-sort.schema.json +104 -0
  239. package/components/ln-sort/src/ln-sort.js +321 -0
  240. package/components/ln-sort/src/sort-model.js +117 -0
  241. package/{js → components}/ln-sortable/README.md +7 -11
  242. package/components/ln-sortable/ln-sortable-dev.scss +13 -0
  243. package/components/ln-sortable/ln-sortable.js +1 -0
  244. package/components/ln-sortable/ln-sortable.schema.json +36 -0
  245. package/{js → components}/ln-sortable/src/ln-sortable.js +8 -13
  246. package/{js → components}/ln-stat/README.md +3 -1
  247. package/components/ln-stat/ln-stat-dev.scss +8 -0
  248. package/components/ln-stat/ln-stat.js +1 -0
  249. package/components/ln-stat/ln-stat.schema.json +32 -0
  250. package/{js → components}/ln-stat/src/ln-stat.js +14 -18
  251. package/components/ln-stat/src/stat-model.js +28 -0
  252. package/{js → components}/ln-table/README.md +37 -24
  253. package/{js → components}/ln-table/ln-table-dev.scss +17 -17
  254. package/components/ln-table/ln-table.js +1 -0
  255. package/components/ln-table/ln-table.schema.json +211 -0
  256. package/components/ln-table/ln-table.scss +15 -0
  257. package/{js → components}/ln-table/src/ln-table.js +1365 -1493
  258. package/components/ln-table/src/table-model.js +98 -0
  259. package/components/ln-table-coordinator/README.md +61 -0
  260. package/components/ln-table-coordinator/ln-table-coordinator.js +1 -0
  261. package/components/ln-table-coordinator/ln-table-coordinator.schema.json +93 -0
  262. package/components/ln-table-coordinator/src/ln-table-coordinator.js +193 -0
  263. package/{js → components}/ln-tabs/README.md +32 -14
  264. package/components/ln-tabs/ln-tabs-dev.scss +24 -0
  265. package/components/ln-tabs/ln-tabs.js +1 -0
  266. package/components/ln-tabs/ln-tabs.schema.json +75 -0
  267. package/components/ln-tabs/ln-tabs.scss +8 -0
  268. package/components/ln-tabs/src/ln-tabs.js +207 -0
  269. package/components/ln-tabs/src/tabs-model.js +82 -0
  270. package/{js → components}/ln-time/README.md +96 -95
  271. package/components/ln-time/ln-time-dev.scss +13 -0
  272. package/components/ln-time/ln-time.js +1 -0
  273. package/components/ln-time/ln-time.schema.json +38 -0
  274. package/{js → components}/ln-time/src/ln-time.js +206 -223
  275. package/components/ln-time/src/time-model.js +64 -0
  276. package/{js → components}/ln-toast/README.md +23 -5
  277. package/components/ln-toast/ln-toast-dev.scss +8 -0
  278. package/components/ln-toast/ln-toast.js +1 -0
  279. package/components/ln-toast/ln-toast.schema.json +65 -0
  280. package/components/ln-toast/ln-toast.scss +59 -0
  281. package/{js → components}/ln-toast/src/ln-toast.js +233 -232
  282. package/{js → components}/ln-toggle/README.md +198 -196
  283. package/{js → components}/ln-toggle/ln-toggle-dev.scss +13 -13
  284. package/components/ln-toggle/ln-toggle.js +1 -0
  285. package/components/ln-toggle/ln-toggle.schema.json +52 -0
  286. package/components/ln-toggle/ln-toggle.scss +37 -0
  287. package/{js → components}/ln-toggle/src/ln-toggle.js +61 -46
  288. package/{js → components}/ln-tooltip/README.md +6 -6
  289. package/{js → components}/ln-tooltip/ln-tooltip-dev.scss +9 -9
  290. package/components/ln-tooltip/ln-tooltip.js +1 -0
  291. package/components/ln-tooltip/ln-tooltip.schema.json +86 -0
  292. package/components/ln-tooltip/ln-tooltip.scss +9 -0
  293. package/{js → components}/ln-tooltip/src/ln-tooltip.js +14 -3
  294. package/{js → components}/ln-translations/README.md +3 -1
  295. package/components/ln-translations/ln-translations-dev.scss +13 -0
  296. package/components/ln-translations/ln-translations.js +1 -0
  297. package/components/ln-translations/ln-translations.schema.json +120 -0
  298. package/{js → components}/ln-translations/src/ln-translations.js +25 -10
  299. package/components/ln-ui-coordinator/README.md +101 -0
  300. package/components/ln-ui-coordinator/ln-ui-coordinator-dev.scss +8 -0
  301. package/components/ln-ui-coordinator/ln-ui-coordinator.js +1 -0
  302. package/components/ln-ui-coordinator/ln-ui-coordinator.schema.json +60 -0
  303. package/{js/ln-modal-coordinator/src/ln-modal-coordinator.js → components/ln-ui-coordinator/src/ln-ui-coordinator.js} +403 -345
  304. package/components/ln-upload/README.md +133 -0
  305. package/components/ln-upload/ln-upload-dev.scss +18 -0
  306. package/components/ln-upload/ln-upload.js +1 -0
  307. package/components/ln-upload/ln-upload.schema.json +193 -0
  308. package/components/ln-upload/ln-upload.scss +20 -0
  309. package/components/ln-upload/src/ln-upload.js +652 -0
  310. package/components/ln-upload/src/upload-model.js +78 -0
  311. package/{js → components}/ln-validate/README.md +182 -180
  312. package/{js → components}/ln-validate/ln-validate-dev.scss +15 -15
  313. package/components/ln-validate/ln-validate.js +1 -0
  314. package/components/ln-validate/ln-validate.schema.json +50 -0
  315. package/components/ln-validate/ln-validate.scss +7 -0
  316. package/{js → components}/ln-validate/src/ln-validate.js +23 -45
  317. package/components/ln-validate/src/validate-model.js +64 -0
  318. package/components/ln-websocket-connector/README.md +166 -0
  319. package/components/ln-websocket-connector/ln-websocket-connector.js +1 -0
  320. package/components/ln-websocket-connector/ln-websocket-connector.schema.json +29 -0
  321. package/components/ln-websocket-connector/src/ln-websocket-connector.js +279 -0
  322. package/ln-ashlar.html-data.json +1796 -0
  323. package/package.json +84 -61
  324. package/{scss → theme}/base/_global.scss +1 -2
  325. package/{scss → theme}/base/_reset.scss +1 -1
  326. package/{scss → theme}/base/_typography.scss +2 -0
  327. package/theme/brand.css +73 -0
  328. package/{scss → theme}/components/_app-shell.scss +8 -0
  329. package/theme/components/_button.scss +24 -0
  330. package/theme/components/_chart.scss +5 -0
  331. package/theme/components/_confirm.scss +6 -0
  332. package/{scss → theme}/components/_date.scss +30 -22
  333. package/{scss → theme}/components/_dropdown.scss +6 -1
  334. package/theme/components/_form.scss +140 -0
  335. package/{scss → theme}/components/_layout.scss +6 -0
  336. package/{scss → theme}/components/_ln-table.scss +1 -6
  337. package/theme/components/_picklist.scss +3 -0
  338. package/{scss → theme}/components/_sort.scss +10 -10
  339. package/theme/components/_status-badge.scss +6 -0
  340. package/{scss → theme}/components/_table.scss +16 -0
  341. package/{scss → theme}/components/_tabs.scss +0 -4
  342. package/theme/components/_toast.scss +48 -0
  343. package/{scss → theme}/components/_toggle.scss +1 -1
  344. package/theme/components/_upload.scss +45 -0
  345. package/{scss → theme}/config/_breakpoints.scss +1 -1
  346. package/theme/config/_contrast.scss +128 -0
  347. package/{scss → theme}/config/_density.scss +191 -205
  348. package/theme/config/_palette.scss +274 -0
  349. package/theme/config/_theme.scss +293 -0
  350. package/{scss → theme}/config/_tokens.scss +285 -441
  351. package/{scss → theme}/config/mixins/_accordion.scss +1 -1
  352. package/{scss → theme}/config/mixins/_alert.scss +1 -0
  353. package/{scss → theme}/config/mixins/_app-shell.scss +4 -2
  354. package/{scss → theme}/config/mixins/_borders.scss +2 -0
  355. package/theme/config/mixins/_btn.scss +175 -0
  356. package/{scss → theme}/config/mixins/_card.scss +55 -46
  357. package/theme/config/mixins/_chart.scss +82 -0
  358. package/{scss → theme}/config/mixins/_collapsible.scss +2 -1
  359. package/{scss → theme}/config/mixins/_colors.scss +8 -14
  360. package/{scss → theme}/config/mixins/_diagnostics.scss +48 -48
  361. package/theme/config/mixins/_dropdown.scss +169 -0
  362. package/{scss → theme}/config/mixins/_editor.scss +0 -7
  363. package/{scss → theme}/config/mixins/_filter.scss +47 -47
  364. package/{scss → theme}/config/mixins/_footer.scss +1 -1
  365. package/{scss → theme}/config/mixins/_form.scss +49 -18
  366. package/{scss → theme}/config/mixins/_index.scss +62 -61
  367. package/{scss → theme}/config/mixins/_layout.scss +6 -2
  368. package/{scss → theme}/config/mixins/_ln-table.scss +31 -10
  369. package/{scss → theme}/config/mixins/_loader.scss +1 -1
  370. package/{scss → theme}/config/mixins/_modal.scss +145 -149
  371. package/{scss → theme}/config/mixins/_motion.scss +13 -0
  372. package/{scss → theme}/config/mixins/_nav.scss +7 -7
  373. package/theme/config/mixins/_picklist.scss +66 -0
  374. package/theme/config/mixins/_popover.scss +96 -0
  375. package/{scss → theme}/config/mixins/_sidebar.scss +7 -4
  376. package/{scss → theme}/config/mixins/_sort.scss +43 -42
  377. package/{scss → theme}/config/mixins/_status-badge.scss +15 -2
  378. package/{scss → theme}/config/mixins/_table.scss +25 -14
  379. package/{scss → theme}/config/mixins/_tabs.scss +1 -1
  380. package/{scss → theme}/config/mixins/_toast.scss +174 -179
  381. package/{scss → theme}/config/mixins/_tooltip.scss +2 -2
  382. package/{scss → theme}/config/mixins/_typography.scss +16 -5
  383. package/{scss → theme}/config/mixins/_upload.scss +8 -6
  384. package/theme/ln-ashlar-core.scss +24 -0
  385. package/theme/ln-ashlar-dev.scss +66 -0
  386. package/theme/ln-ashlar-landing.scss +56 -0
  387. package/{scss/ln-ashlar.scss → theme/ln-ashlar-theme.scss} +63 -69
  388. package/theme/ln-ashlar.scss +8 -0
  389. package/theme/presentation/_bento.scss +151 -0
  390. package/theme/presentation/_cta.scss +111 -0
  391. package/theme/presentation/_effects.scss +216 -0
  392. package/theme/presentation/_features.scss +108 -0
  393. package/theme/presentation/_footer.scss +129 -0
  394. package/theme/presentation/_hero.scss +158 -0
  395. package/theme/presentation/_layout.scss +170 -0
  396. package/theme/presentation/_navbar.scss +140 -0
  397. package/theme/presentation/_pricing.scss +262 -0
  398. package/theme/presentation/_stats.scss +87 -0
  399. package/theme/presentation/_testimonials.scss +124 -0
  400. package/theme/presentation/_tokens.scss +39 -0
  401. package/theme/presentation/_typography.scss +163 -0
  402. package/{scss → theme}/utilities/_animations.scss +38 -0
  403. package/{scss → theme}/utilities/_utilities.scss +6 -0
  404. package/web-types.json +2312 -0
  405. package/js/ln-accordion/ln-accordion.js +0 -1
  406. package/js/ln-ajax/ln-ajax.js +0 -1
  407. package/js/ln-api-connector/ln-api-connector.js +0 -1
  408. package/js/ln-api-connector/src/ln-api-connector.js +0 -368
  409. package/js/ln-api-queue/ln-api-queue.js +0 -1
  410. package/js/ln-autoresize/ln-autoresize.js +0 -1
  411. package/js/ln-autosave/ln-autosave.js +0 -1
  412. package/js/ln-circular-progress/ln-circular-progress.js +0 -1
  413. package/js/ln-confirm/_ln-confirm.scss +0 -24
  414. package/js/ln-confirm/ln-confirm.js +0 -1
  415. package/js/ln-core/crypto.js +0 -83
  416. package/js/ln-core/helpers.js +0 -788
  417. package/js/ln-core/index.js +0 -7
  418. package/js/ln-core/persist.js +0 -97
  419. package/js/ln-couchdb-connector/ln-couchdb-connector.js +0 -1
  420. package/js/ln-data-coordinator/ln-data-coordinator.js +0 -1
  421. package/js/ln-data-coordinator/src/data-read-policy.js +0 -18
  422. package/js/ln-data-store/ln-data-store.js +0 -1
  423. package/js/ln-date/ln-date.js +0 -1
  424. package/js/ln-date/src/ln-date.js +0 -603
  425. package/js/ln-debug/README.md +0 -58
  426. package/js/ln-debug/ln-debug.js +0 -1
  427. package/js/ln-debug/src/ln-debug.js +0 -19
  428. package/js/ln-dropdown/ln-dropdown.js +0 -1
  429. package/js/ln-dropdown/ln-dropdown.scss +0 -15
  430. package/js/ln-editor/ln-editor.js +0 -1
  431. package/js/ln-editor/ln-editor.scss +0 -43
  432. package/js/ln-external-links/ln-external-links.js +0 -1
  433. package/js/ln-fill/ln-fill.js +0 -1
  434. package/js/ln-filter/README.md +0 -189
  435. package/js/ln-filter/ln-filter-dev.scss +0 -8
  436. package/js/ln-filter/ln-filter.js +0 -1
  437. package/js/ln-filter/src/ln-filter.js +0 -419
  438. package/js/ln-form/ln-form.js +0 -1
  439. package/js/ln-http/ln-http.js +0 -1
  440. package/js/ln-link/ln-link.js +0 -1
  441. package/js/ln-list/README.md +0 -129
  442. package/js/ln-list/ln-list.js +0 -1
  443. package/js/ln-list/ln-list.scss +0 -28
  444. package/js/ln-modal/ln-modal-dev.scss +0 -13
  445. package/js/ln-modal/ln-modal.js +0 -1
  446. package/js/ln-modal/ln-modal.scss +0 -35
  447. package/js/ln-modal-coordinator/README.md +0 -99
  448. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +0 -8
  449. package/js/ln-modal-coordinator/ln-modal-coordinator.js +0 -1
  450. package/js/ln-nav/ln-nav.js +0 -1
  451. package/js/ln-number/README.md +0 -149
  452. package/js/ln-number/ln-number.js +0 -1
  453. package/js/ln-number/src/ln-number.js +0 -443
  454. package/js/ln-options/ln-options.js +0 -1
  455. package/js/ln-popover/ln-popover.js +0 -1
  456. package/js/ln-progress/ln-progress.js +0 -1
  457. package/js/ln-progress/src/ln-progress.js +0 -98
  458. package/js/ln-router/ln-router.js +0 -1
  459. package/js/ln-search/README.md +0 -158
  460. package/js/ln-search/ln-search-dev.scss +0 -8
  461. package/js/ln-search/ln-search.js +0 -1
  462. package/js/ln-search/src/ln-search.js +0 -117
  463. package/js/ln-slug/ln-slug.js +0 -1
  464. package/js/ln-sort/ln-sort.js +0 -1
  465. package/js/ln-sort/src/ln-sort.js +0 -178
  466. package/js/ln-sortable/ln-sortable.js +0 -1
  467. package/js/ln-stat/ln-stat.js +0 -1
  468. package/js/ln-table/ln-table.js +0 -1
  469. package/js/ln-table/ln-table.scss +0 -11
  470. package/js/ln-table-coordinator/README.md +0 -73
  471. package/js/ln-table-coordinator/ln-table-coordinator.js +0 -1
  472. package/js/ln-table-coordinator/src/ln-table-coordinator.js +0 -160
  473. package/js/ln-tabs/ln-tabs-dev.scss +0 -18
  474. package/js/ln-tabs/ln-tabs.js +0 -1
  475. package/js/ln-tabs/src/ln-tabs.js +0 -188
  476. package/js/ln-time/ln-time.js +0 -1
  477. package/js/ln-toast/ln-toast.js +0 -1
  478. package/js/ln-toggle/ln-toggle.js +0 -1
  479. package/js/ln-tooltip/ln-tooltip.js +0 -1
  480. package/js/ln-tooltip/ln-tooltip.scss +0 -9
  481. package/js/ln-translations/ln-translations.js +0 -1
  482. package/js/ln-upload/README.md +0 -255
  483. package/js/ln-upload/ln-upload.js +0 -1
  484. package/js/ln-upload/ln-upload.scss +0 -20
  485. package/js/ln-upload/src/ln-upload.js +0 -423
  486. package/js/ln-validate/ln-validate.js +0 -1
  487. package/scss/components/_button.scss +0 -20
  488. package/scss/components/_confirm.scss +0 -5
  489. package/scss/components/_form.scss +0 -107
  490. package/scss/components/_status-badge.scss +0 -4
  491. package/scss/components/_toast.scss +0 -68
  492. package/scss/components/_upload.scss +0 -51
  493. package/scss/config/_theme.scss +0 -216
  494. package/scss/config/mixins/_btn.scss +0 -179
  495. package/scss/config/mixins/_confirm.scss +0 -31
  496. package/scss/config/mixins/_dropdown.scss +0 -141
  497. package/scss/config/mixins/_popover.scss +0 -64
  498. package/scss/ln-ashlar-dev.scss +0 -39
  499. /package/{js → components}/ln-core/positioning.js +0 -0
  500. /package/{js → components}/ln-core/reactive.js +0 -0
  501. /package/{js → components}/ln-filter/ln-filter.scss +0 -0
  502. /package/{js → components}/ln-icon/ln-icon.js +0 -0
  503. /package/{js → components}/ln-icon/src/ln-icon.js +0 -0
  504. /package/{js → components}/ln-search/ln-search.scss +0 -0
  505. /package/{js → components}/ln-toast/template.html +0 -0
  506. /package/{scss → theme}/components/_accordion.scss +0 -0
  507. /package/{scss → theme}/components/_ajax.scss +0 -0
  508. /package/{scss → theme}/components/_alert.scss +0 -0
  509. /package/{scss → theme}/components/_avatar.scss +0 -0
  510. /package/{scss → theme}/components/_breadcrumbs.scss +0 -0
  511. /package/{scss → theme}/components/_card.scss +0 -0
  512. /package/{scss → theme}/components/_chip.scss +0 -0
  513. /package/{scss → theme}/components/_circular-progress.scss +0 -0
  514. /package/{scss → theme}/components/_editor.scss +0 -0
  515. /package/{scss → theme}/components/_empty-state.scss +0 -0
  516. /package/{scss → theme}/components/_filter.scss +0 -0
  517. /package/{scss → theme}/components/_link.scss +0 -0
  518. /package/{scss → theme}/components/_loader.scss +0 -0
  519. /package/{scss → theme}/components/_modal.scss +0 -0
  520. /package/{scss → theme}/components/_nav.scss +0 -0
  521. /package/{scss → theme}/components/_page-header.scss +0 -0
  522. /package/{scss → theme}/components/_popover.scss +0 -0
  523. /package/{scss → theme}/components/_progress.scss +0 -0
  524. /package/{scss → theme}/components/_prose.scss +0 -0
  525. /package/{scss → theme}/components/_scrollbar.scss +0 -0
  526. /package/{scss → theme}/components/_sections.scss +0 -0
  527. /package/{scss → theme}/components/_sidebar.scss +0 -0
  528. /package/{scss → theme}/components/_stat-card.scss +0 -0
  529. /package/{scss → theme}/components/_stepper.scss +0 -0
  530. /package/{scss → theme}/components/_timeline.scss +0 -0
  531. /package/{scss → theme}/components/_tooltip.scss +0 -0
  532. /package/{scss → theme}/components/_translations.scss +0 -0
  533. /package/{scss → theme}/config/_icons.scss +0 -0
  534. /package/{scss → theme}/config/_mixins.scss +0 -0
  535. /package/{scss → theme}/config/mixins/_ajax.scss +0 -0
  536. /package/{scss → theme}/config/mixins/_avatar.scss +0 -0
  537. /package/{scss → theme}/config/mixins/_breadcrumbs.scss +0 -0
  538. /package/{scss → theme}/config/mixins/_breakpoints.scss +0 -0
  539. /package/{scss → theme}/config/mixins/_chip.scss +0 -0
  540. /package/{scss → theme}/config/mixins/_circular-progress.scss +0 -0
  541. /package/{scss → theme}/config/mixins/_display.scss +0 -0
  542. /package/{scss → theme}/config/mixins/_empty-state.scss +0 -0
  543. /package/{scss → theme}/config/mixins/_focus.scss +0 -0
  544. /package/{scss → theme}/config/mixins/_interaction.scss +0 -0
  545. /package/{scss → theme}/config/mixins/_kbd.scss +0 -0
  546. /package/{scss → theme}/config/mixins/_link.scss +0 -0
  547. /package/{scss → theme}/config/mixins/_page-header.scss +0 -0
  548. /package/{scss → theme}/config/mixins/_position.scss +0 -0
  549. /package/{scss → theme}/config/mixins/_progress.scss +0 -0
  550. /package/{scss → theme}/config/mixins/_prose.scss +0 -0
  551. /package/{scss → theme}/config/mixins/_shadows.scss +0 -0
  552. /package/{scss → theme}/config/mixins/_sizing.scss +0 -0
  553. /package/{scss → theme}/config/mixins/_spacing.scss +0 -0
  554. /package/{scss → theme}/config/mixins/_stat-card.scss +0 -0
  555. /package/{scss → theme}/config/mixins/_stepper.scss +0 -0
  556. /package/{scss → theme}/config/mixins/_timeline.scss +0 -0
  557. /package/{scss → theme}/config/mixins/_transitions.scss +0 -0
  558. /package/{scss → theme}/config/mixins/_translations.scss +0 -0
  559. /package/{scss → theme}/tabler-icons.txt +0 -0
@@ -1,124 +1,127 @@
1
- # ln-autosave
2
-
3
- A zero-dependency, localStorage-backed **Draft Buffer Primitive** that prevents data loss by capturing form states. It automatically saves drafts on field boundaries, restores values on load, and clears them cleanly on submit, reset, or custom cancellations.
4
-
5
- ---
6
-
7
- ## 🧭 Philosophy & Architecture
8
-
9
- 1. **Synchronous local storage:** Unlike async network databases, `ln-autosave` uses browser `localStorage`. This ensures drafts are written instantly on field blur/leave (`focusout` / `change`), eliminating race conditions during page changes.
10
- 2. **Zero-Keystroke flood:** By default, writing only triggers on field blur. For continuous typing fields (like editors), an opt-in debounced input listener can be configured.
11
- 3. **Decoupled Event Restoration:** restored values are applied by dispatching standard synthetic `input` and `change` events. This ensures sibling primitives (such as `ln-validate` and `ln-autoresize`) re-evaluate automatically.
12
-
13
- ---
14
-
15
- ## 📦 Minimal Blueprint
16
-
17
- ```html
18
- <form id="profile-edit" data-ln-autosave>
19
- <div class="form-element">
20
- <label for="bio">Biography</label>
21
- <textarea id="bio" name="bio" data-ln-autoresize rows="1"></textarea>
22
- </div>
23
-
24
- <ul class="form-actions">
25
- <li><button type="button" data-ln-autosave-clear>Cancel (clear draft)</button></li>
26
- <li><button type="submit">Save</button></li>
27
- </ul>
28
- </form>
29
- ```
30
-
31
- > [!IMPORTANT]
32
- > The form **must** have a unique `id` or you must specify a custom identifier in `data-ln-autosave="identifier"`. The final storage key is scoped uniquely as `ln-autosave:{pathname}:{identifier}`.
33
-
34
- ---
35
-
36
- ## 🛠️ Declarative API Contract
37
-
38
- ### HTML Attributes
39
-
40
- | Attribute | Elements | Description |
41
- | :--- | :--- | :--- |
42
- | `data-ln-autosave` | `<form>` | Persistence marker. Value overrides the form ID lookup. |
43
- | `data-ln-autosave-clear` | `<button>` | Click delegate. Instantly purges the localStorage entry. |
44
- | `data-ln-autosave-debounce-input="ms"` | `<form>` | Opt-in. Saves on idle keystrokes (defaults to 1000ms if empty). |
45
-
46
- ### JS API
47
-
48
- Access the persistence instance directly via the `lnAutosave` property on the form element:
49
-
50
- ```javascript
51
- const form = document.getElementById('profile-edit');
52
-
53
- // 1. Back-reference properties
54
- const storageKey = form.lnAutosave.key; // "ln-autosave:/path:profile-edit"
55
-
56
- // 2. Clean up listeners and pending timers
57
- form.lnAutosave.destroy();
58
- ```
59
-
60
- ---
61
-
62
- ## ⚡ DOM Events
63
-
64
- ### Emitted
65
-
66
- | Event | Bubbles | Payload | Description |
67
- | :--- | :--- | :--- | :--- |
68
- | `ln-autosave:before-restore` | Yes (Cancelable) | `{ target, data }` | Fires before applying draft. Call `e.preventDefault()` to abort. |
69
- | `ln-autosave:restored` | Yes | `{ target, data }` | Dispatched after populated values and synthetic events are sent. |
70
- | `ln-autosave:saved` | Yes | `{ target, data }` | Dispatched on successful localStorage save. |
71
- | `ln-autosave:cleared` | Yes | `{ target }` | Dispatched after localStorage item removal. |
72
-
73
- ---
74
-
75
- ## ⚠️ Common Pitfalls
76
-
77
- - **Stale Drafts Overwriting Server Data:** On pages where the database has rendered new state, draft restores must be aborted:
78
- ```javascript
79
- document.addEventListener('ln-autosave:before-restore', function (e) {
80
- if (e.target.dataset.hasServerData === 'true') {
81
- e.preventDefault();
82
- localStorage.removeItem(e.target.lnAutosave.key);
83
- }
84
- });
85
- ```
86
- - **ESC Modal Dismissals:** If a user closes a modal using the ESC key, no click triggers on cancel buttons. Wire draft cleanup to the modal's closed event manually:
87
- ```javascript
88
- modal.addEventListener('ln-modal:closed', () => {
89
- localStorage.removeItem(form.lnAutosave.key);
90
- });
91
- ```
92
- - **Nameless & File Inputs:** `ln-autosave` ignores disabled inputs, button elements, `type="file"`, and inputs missing a `name` attribute.
93
-
94
- ---
95
-
96
- ## 🔧 Internals
97
-
98
- Source: `js/ln-autosave/ln-autosave.js` (~130 lines). No `.scss` file — the component has no visual surface, no class it sets, no CSS-driving attribute.
99
-
100
- ### State
101
-
102
- Per-form instance (`form.lnAutosave`): `dom`, `key` (resolved once at construction — `ln-autosave:{pathname}:{identifier}`; mutating the attribute, `form.id`, or navigating via the History API afterwards does NOT re-key — destroy and re-mount to switch), and the bound handlers (`_onFocusout`, `_onChange`, `_onSubmit`, `_onReset`, `_onClearClick`, plus `_onInput` only when debounce is opted in) held so `destroy()` can pass matching references to `removeEventListener`.
103
-
104
- ### Storage key
105
-
106
- `_getStorageKey` resolves `identifier` from the attribute value if non-empty, else `form.id`. Empty attribute (`data-ln-autosave=""`) falls through to `form.id`; missing both returns `null` and construction aborts with a console warning — `form.lnAutosave` is never set.
107
-
108
- ### Save / restore / clear
109
-
110
- - **Save** (`focusout`, `change`, optional debounced `input`): `serializeForm` → `localStorage.setItem`, wrapped in a silent `try/catch` (`QuotaExceededError`, storage disabled). A failed write fires no event — there is no `:save-failed`.
111
- - **Restore** (construction-time only): read + parse (silent try/catch) → cancelable `:before-restore` → `populateForm` → dispatch synthetic `input` **and** `change` on every restored field (covers both signal types regardless of field kind: text/textarea react to `input`, checkbox/radio/select to `change`) → `:restored`. Events fire after all fields are written, so cross-field validation sees the fully-restored form.
112
- - **Clear**: `localStorage.removeItem` (idempotent) → `:cleared`. Triggered by native `submit` (not by `form.submit()` — that call bypasses the event; use `requestSubmit()`), native `reset`, or a click on `[data-ln-autosave-clear]`. Touches only the storage entry, never field values.
113
-
114
- ### Debounce is input-only, opt-in
115
-
116
- `focusout`/`change` are naturally rate-limited by user behaviour (one event per field visit) — no debounce needed. `input` fires per keystroke, so `data-ln-autosave-debounce-input` exists as an explicit opt-in for compose-style fields the user never blurs. Default off. The interval is read once at construction (`_resolveDebounceMs`); mutating the attribute afterwards has no effect on a live instance.
117
-
118
- ### MutationObserver
119
-
120
- `registerComponent` watches `childList` (new `[data-ln-autosave]` forms) and `attributes` (filtered to `data-ln-autosave` only — NOT `-clear` or `-debounce-input`). The clear-button delegate uses `closest()` per click, so `data-ln-autosave-clear` works even if added after construction; the debounce attribute does not, since it's read once.
121
-
122
- ### Destroy
123
-
124
- Removes all five listeners (plus `input` and its pending timer if debounce was active), dispatches `:destroyed`, deletes the instance. Does **not** call `_clear()` — the draft persists after destroy; destroy means "stop autosaving," not "discard the draft."
1
+ # ln-autosave
2
+
3
+ > Applied to a `<form id="..." data-ln-autosave>`. On initialization, it reads draft JSON from `localStorage.getItem(key)`
4
+ > and populates form fields, dispatching synthetic `input` and `change` events. On field `focusout` or `change`, it serializes
5
+ > form values — strictly excluding passwords (`input[type="password"]`) and `[data-ln-autosave-exclude]` — and saves them
6
+ > via `localStorage.setItem(key, json)`. On form `submit`, `reset`, or click on `[data-ln-autosave-clear]`, it calls `localStorage.removeItem(key)` to purge the draft.
7
+
8
+ ---
9
+
10
+ ## 🧭 Philosophy & Architecture
11
+
12
+ 1. **Synchronous local storage:** Unlike async network databases, `ln-autosave` uses browser `localStorage`. This ensures drafts are written instantly on field blur/leave (`focusout` / `change`), eliminating race conditions during page changes.
13
+ 2. **Zero-Keystroke flood:** By default, writing only triggers on field blur. For continuous typing fields (like editors), an opt-in debounced input listener can be configured.
14
+ 3. **Decoupled Event Restoration:** restored values are applied by dispatching standard synthetic `input` and `change` events. This ensures sibling primitives (such as `ln-validate` and `ln-autoresize`) re-evaluate automatically.
15
+
16
+ ---
17
+
18
+ ## 📦 Minimal Blueprint
19
+
20
+ ```html
21
+ <form id="profile-edit" data-ln-autosave>
22
+ <div class="form-element">
23
+ <label for="bio">Biography</label>
24
+ <textarea id="bio" name="bio" data-ln-autoresize rows="1"></textarea>
25
+ </div>
26
+
27
+ <ul class="form-actions">
28
+ <li><button type="button" data-ln-autosave-clear>Cancel (clear draft)</button></li>
29
+ <li><button type="submit">Save</button></li>
30
+ </ul>
31
+ </form>
32
+ ```
33
+
34
+ > [!IMPORTANT]
35
+ > The form **must** have a unique `id` or you must specify a custom identifier in `data-ln-autosave="identifier"`. The final storage key is scoped uniquely as `ln-autosave:{pathname}:{identifier}`.
36
+
37
+ ---
38
+
39
+ ## 🛠️ Declarative API Contract
40
+
41
+ ### HTML Attributes
42
+
43
+ | Attribute | Elements | Description |
44
+ | :--- | :--- | :--- |
45
+ | `data-ln-autosave` | `<form>` | Persistence marker. Value overrides the form ID lookup. |
46
+ | `data-ln-autosave-clear` | `<button>` | Click delegate. Instantly purges the localStorage entry. |
47
+ | `data-ln-autosave-debounce-input="ms"` | `<form>` | Opt-in. Saves on idle keystrokes (defaults to 1000ms if empty). |
48
+
49
+ ### JS API
50
+
51
+ Access the persistence instance directly via the `lnAutosave` property on the form element:
52
+
53
+ ```javascript
54
+ const form = document.getElementById('profile-edit');
55
+
56
+ // 1. Back-reference properties
57
+ const storageKey = form.lnAutosave.key; // "ln-autosave:/path:profile-edit"
58
+
59
+ // 2. Clean up listeners and pending timers
60
+ form.lnAutosave.destroy();
61
+ ```
62
+
63
+ ---
64
+
65
+ ## ⚡ DOM Events
66
+
67
+ ### Emitted
68
+
69
+ | Event | Bubbles | Payload | Description |
70
+ | :--- | :--- | :--- | :--- |
71
+ | `ln-autosave:before-restore` | Yes (Cancelable) | `{ target, data }` | Fires before applying draft. Call `e.preventDefault()` to abort. |
72
+ | `ln-autosave:restored` | Yes | `{ target, data }` | Dispatched after populated values and synthetic events are sent. |
73
+ | `ln-autosave:saved` | Yes | `{ target, data }` | Dispatched on successful localStorage save. |
74
+ | `ln-autosave:cleared` | Yes | `{ target }` | Dispatched after localStorage item removal. |
75
+
76
+ ---
77
+
78
+ ## ⚠️ Common Pitfalls
79
+
80
+ - **Stale Drafts Overwriting Server Data:** On pages where the database has rendered new state, draft restores must be aborted:
81
+ ```javascript
82
+ document.addEventListener('ln-autosave:before-restore', function (e) {
83
+ if (e.target.dataset.hasServerData === 'true') {
84
+ e.preventDefault();
85
+ localStorage.removeItem(e.target.lnAutosave.key);
86
+ }
87
+ });
88
+ ```
89
+ - **ESC Modal Dismissals:** If a user closes a modal using the ESC key, no click triggers on cancel buttons. Wire draft cleanup to the modal's closed event manually:
90
+ ```javascript
91
+ modal.addEventListener('ln-modal:closed', () => {
92
+ localStorage.removeItem(form.lnAutosave.key);
93
+ });
94
+ ```
95
+ - **Excluded Inputs:** `ln-autosave` automatically ignores disabled inputs, button elements, `type="file"`, `type="password"`, inputs missing a `name` attribute, and any element matching `[data-ln-autosave-exclude]`.
96
+
97
+ ---
98
+
99
+ ## 🔧 Internals
100
+
101
+ Source: `components/ln-autosave/ln-autosave.js` (~150 lines). No `.scss` file — the component has no visual surface, no class it sets, no CSS-driving attribute.
102
+
103
+ ### State
104
+
105
+ Per-form instance (`form.lnAutosave`): `dom`, `key` (resolved once at construction — `ln-autosave:{pathname}:{identifier}`; mutating the attribute, `form.id`, or navigating via the History API afterwards does NOT re-key — destroy and re-mount to switch), and the bound handlers (`_onFocusout`, `_onChange`, `_onSubmit`, `_onReset`, `_onClearClick`, plus `_onInput` only when debounce is opted in) held so `destroy()` can pass matching references to `removeEventListener`.
106
+
107
+ ### Storage key
108
+
109
+ `_getStorageKey` resolves `identifier` from the attribute value if non-empty, else `form.id`. Empty attribute (`data-ln-autosave=""`) falls through to `form.id`; missing both returns `null` and construction aborts with a console warning — `form.lnAutosave` is never set.
110
+
111
+ ### Save / restore / clear
112
+
113
+ - **Save** (`focusout`, `change`, optional debounced `input`): `serializeForm` → `localStorage.setItem`, wrapped in a silent `try/catch` (`QuotaExceededError`, storage disabled). A failed write fires no event — there is no `:save-failed`.
114
+ - **Restore** (construction-time only): read + parse (silent try/catch) → cancelable `:before-restore` → `populateForm` → dispatch synthetic `input` **and** `change` on every restored field (covers both signal types regardless of field kind: text/textarea react to `input`, checkbox/radio/select to `change`) → `:restored`. Events fire after all fields are written, so cross-field validation sees the fully-restored form.
115
+ - **Clear**: `localStorage.removeItem` (idempotent) → `:cleared`. Triggered by native `submit` (not by `form.submit()` — that call bypasses the event; use `requestSubmit()`), native `reset`, or a click on `[data-ln-autosave-clear]`. Touches only the storage entry, never field values.
116
+
117
+ ### Debounce is input-only, opt-in
118
+
119
+ `focusout`/`change` are naturally rate-limited by user behaviour (one event per field visit) — no debounce needed. `input` fires per keystroke, so `data-ln-autosave-debounce-input` exists as an explicit opt-in for compose-style fields the user never blurs. Default off. The interval is read once at construction (`_resolveDebounceMs`); mutating the attribute afterwards has no effect on a live instance.
120
+
121
+ ### MutationObserver
122
+
123
+ `registerComponent` watches `childList` (new `[data-ln-autosave]` forms) and `attributes` (filtered to `data-ln-autosave` only — NOT `-clear` or `-debounce-input`). The clear-button delegate uses `closest()` per click, so `data-ln-autosave-clear` works even if added after construction; the debounce attribute does not, since it's read once.
124
+
125
+ ### Destroy
126
+
127
+ Removes all five listeners (plus `input` and its pending timer if debounce was active), deletes the instance. Does **not** call `_clear()` — the draft persists after destroy; destroy means "stop autosaving," not "discard the draft."
@@ -0,0 +1,13 @@
1
+ @use '../../theme/config/mixins' as *;
2
+
3
+ [data-ln-debug] {
4
+ // 1. Host Element Check: data-ln-autosave belongs only on <form>
5
+ [data-ln-autosave]:not(form) {
6
+ @include dev-inline-error("data-ln-autosave belongs only on <form> elements");
7
+ }
8
+
9
+ // 2. Storage Key Check: form must have an id or explicit data-ln-autosave key
10
+ form[data-ln-autosave=""]:not([id]) {
11
+ @include dev-dom-error("form[data-ln-autosave] requires an 'id' or explicit storage key value.");
12
+ }
13
+ }
@@ -0,0 +1 @@
1
+ (function(){"use strict";function D(){return typeof window>"u"?!1:window.lnDebug===!0||window.lnCore&&window.lnCore._debugSink?!0:typeof document<"u"&&document.body?document.body.hasAttribute("data-ln-debug")||document.body.querySelector("[data-ln-debug]")!==null:!1}function A(e){if(typeof window>"u")return!1;if(window.lnDebug===!0)return!0;if(window.lnCore&&window.lnCore._debugIsContained)return e?window.lnCore._debugIsContained(e):window.lnCore._debugSink!==null;if(e&&e.closest){const t=e.closest("[data-ln-debug]");return!(!t||typeof document<"u"&&t===document.documentElement)}return typeof document<"u"&&document.body?document.body.hasAttribute("data-ln-debug"):!1}if(typeof window<"u"&&(window.lnCore=window.lnCore||{},!window.lnCore._warnBound)){window.lnCore._warnBound=!0;const e=console.warn;console.warn=function(...t){if(typeof t[0]=="string"&&(t[0].startsWith("[ln-")||t[0].startsWith("[lnCore"))){let i=null;for(let l=1;l<t.length;l++)if(t[l]&&t[l].nodeType===1){i=t[l];break}if(!A(i))return}e.apply(console,t)}}function v(e,t,o){const i=o||{};window.lnCore._debugSink&&window.lnCore._debugSink("event",t,e,i),e.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:i}))}function U(e,t,o){const i=o||{};window.lnCore._debugSink&&window.lnCore._debugSink("event",t,e,i);const l=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:i});return e.dispatchEvent(l),l}function _(e,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){_(e,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}e()}function H(e,t){const o=!!(t&&t.typed),i=t&&t.exclude,l={},s=e.elements,r={};if(o)for(let c=0;c<s.length;c++){const n=s[c];n.name&&n.type==="checkbox"&&!n.disabled&&(r[n.name]=(r[n.name]||0)+1)}for(let c=0;c<s.length;c++){const n=s[c];if(!(!n.name||n.disabled||n.type==="file"||n.type==="submit"||n.type==="button")&&!(i&&n.matches&&n.matches(i)))if(n.type==="checkbox")o&&r[n.name]===1?l[n.name]=n.checked:(l[n.name]||(l[n.name]=[]),n.checked&&l[n.name].push(n.value));else if(n.type==="radio")n.checked&&(l[n.name]=n.value);else if(n.type==="select-multiple"){l[n.name]=[];for(let a=0;a<n.options.length;a++)n.options[a].selected&&l[n.name].push(n.options[a].value)}else if(o&&n.type==="hidden")l[n.name]=n.value;else if(o&&(n.type==="number"||n.type==="range")){const a=Number(n.value);l[n.name]=n.value===""||isNaN(a)?null:a}else l[n.name]=n.value}return l}function V(e){if(typeof e!="string")return!!e;const t=e.trim().toLowerCase();return t!=="false"&&t!=="0"&&t!==""&&t!=="off"&&t!=="no"}function K(e,t){const o=e.elements,i=[],l={};for(let s=0;s<o.length;s++){const r=o[s];r.name&&r.type==="checkbox"&&(l[r.name]=(l[r.name]||0)+1)}for(let s=0;s<o.length;s++){const r=o[s];if(r.type==="file"||r.type==="submit"||r.type==="button")continue;const c=r.getAttribute("data-ln-fill-as")||r.name;if(!c||!(c in t))continue;const n=t[c];if(r.type==="checkbox"){if(Array.isArray(n))r.checked=n.indexOf(r.value)!==-1;else if(l[r.name]>1){const a=String(n).split(",").map(function(d){return d.trim()});r.checked=a.indexOf(r.value)!==-1}else r.checked=V(n);i.push(r)}else if(r.type==="radio")r.checked=r.value===String(n),i.push(r);else if(r.type==="select-multiple"){if(Array.isArray(n))for(let a=0;a<r.options.length;a++)r.options[a].selected=n.indexOf(r.options[a].value)!==-1;i.push(r)}else r.value=n,i.push(r)}return i}const R={};function Q(e,t){R[e]=t}function J(e){return R[e]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=Q,window.lnCore.getDataMapper=J);function P(){typeof window>"u"||(window.lnCore=window.lnCore||{},!window.lnCore._localeObserverBound&&(window.lnCore._localeObserverBound=!0,_(function(){new MutationObserver(function(){v(document,"ln-core:locale-change",{})}).observe(document.documentElement,{attributes:!0,attributeFilter:["lang"],subtree:!0})},"ln-core")))}const $={};function X(e,t){if(!e||typeof t!="object")return;const o=e.toLowerCase().split("-")[0];$[o]=t}function G(e){if(!e)return null;const t=e.toLowerCase().split("-")[0];return $[t]||null}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerLocaleFallback=X,window.lnCore.getLocaleFallback=G,window.lnCore.ensureLocaleObserver=P);const k={};function z(e,t){k[e]||(k[e]=document.querySelector('[data-ln-template="'+e+'"]'));const o=k[e];return o?o.content.cloneNode(!0):(console.warn("["+(t||"ln-core")+'] Template "'+e+'" not found'),null)}function F(e,t){if(!e||!t)return e;const o=e.querySelectorAll("[data-ln-field]");for(let r=0;r<o.length;r++){const c=o[r],n=c.getAttribute("data-ln-field");t[n]!=null&&(c.textContent=t[n])}const i=e.querySelectorAll("[data-ln-attr]");for(let r=0;r<i.length;r++){const c=i[r],n=c.getAttribute("data-ln-attr").split(",");for(let a=0;a<n.length;a++){const d=n[a].trim().split(":");if(d.length!==2)continue;const u=d[0].trim(),h=d[1].trim();t[h]!=null&&c.setAttribute(u,t[h])}}const l=e.querySelectorAll("[data-ln-show]");for(let r=0;r<l.length;r++){const c=l[r],n=c.getAttribute("data-ln-show");n in t&&c.classList.toggle("hidden",!t[n])}const s=e.querySelectorAll("[data-ln-class]");for(let r=0;r<s.length;r++){const c=s[r],n=c.getAttribute("data-ln-class").split(",");for(let a=0;a<n.length;a++){const d=n[a].trim().split(":");if(d.length!==2)continue;const u=d[0].trim(),h=d[1].trim();h in t&&c.classList.toggle(u,!!t[h])}}return e}function Y(e,t){e.matches&&e.matches("[data-ln-form], [data-ln-fillable]")&&(window.lnCore._debugSink&&window.lnCore._debugSink("event","ln-fill",e,t??null),e.dispatchEvent(new CustomEvent("ln-fill",{detail:t??null,bubbles:!0})));const o=e.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let i=0;i<o.length;i++)window.lnCore._debugSink&&window.lnCore._debugSink("event","ln-fill",o[i],t??null),o[i].dispatchEvent(new CustomEvent("ln-fill",{detail:t??null,bubbles:!0}));return e}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(e){if(!(!e.target.matches||!e.target.matches("[data-ln-fillable]")))if(e.detail)F(e.target,e.detail);else{const t=e.target.querySelectorAll("[data-ln-field]");for(let o=0;o<t.length;o++)t[o].textContent=""}})));function N(e,t){if(!e||!t)return e;const o=document.createTreeWalker(e,NodeFilter.SHOW_TEXT);for(;o.nextNode();){const s=o.currentNode;s.textContent.indexOf("{{")!==-1&&(s.textContent=s.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(r,c){return t[c]!==void 0?t[c]:""}))}const i=function(s,r){return t[r]!==void 0?t[r]:""},l=Array.from(e.querySelectorAll("*"));e.nodeType===1&&l.push(e);for(let s=0;s<l.length;s++){const r=l[s],c=r.attributes;for(let n=0;n<c.length;n++){const a=c[n];a.value.indexOf("{{")!==-1&&r.setAttribute(a.name,a.value.replace(/\{\{\s*(\w+)\s*\}\}/g,i))}}return e}function Z(e,t,o,i,l,s){const r={};for(let n=0;n<e.children.length;n++){const a=e.children[n],d=a.getAttribute("data-ln-render-key");d&&(r[d]=a)}const c=document.createDocumentFragment();for(let n=0;n<t.length;n++){const a=t[n],d=String(i(a));let u=r[d];if(u)l(u,a,n);else{const h=z(o,s);if(!h||(N(h,a),u=h.firstElementChild,!u))continue;u.setAttribute("data-ln-render-key",d),l(u,a,n)}c.appendChild(u)}e.textContent="",e.appendChild(c)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.fillTemplate=N,window.lnCore.fill=F,window.lnCore.lnFill=Y,window.lnCore.renderList=Z);function q(e,t,o,i,l){if(!e||t===null)return!0;const s=i||"ln-component",r=e.type;if(r==="trigger"||r==="marker"||r==="string"||r==="list"||!r||t===""&&e.fallback!==void 0)return!0;const c=n=>{l?console.warn(n,l):console.warn(n)};if(r==="boolean"){const n=t.trim().toLowerCase();return n!==""&&n!=="true"&&n!=="false"&&n!=="1"&&n!=="0"?(c(`[${s}] Invalid value "${t}" for boolean attribute "${o}". Allowed: "true", "false", or presence-only.`),!1):!0}if(r==="enum"){const n=e.values||[];return n.includes(t)?!0:(c(`[${s}] Invalid value "${t}" for attribute "${o}". Allowed: ${n.join(", ")}. Fallback: "${e.fallback}".`),!1)}if(r==="integer"){if(!/^-?\d+$/.test(t))return c(`[${s}] Invalid integer "${t}" for attribute "${o}". Fallback: ${e.fallback}.`),!1;const n=parseInt(t,10);return e.min!==void 0&&n<e.min?(c(`[${s}] Value ${n} for attribute "${o}" is less than min (${e.min}).`),!1):e.max!==void 0&&n>e.max?(c(`[${s}] Value ${n} for attribute "${o}" is greater than max (${e.max}).`),!1):!0}if(r==="float")return isNaN(Number(t))?(c(`[${s}] Invalid float "${t}" for attribute "${o}". Fallback: ${e.fallback}.`),!1):!0;if(r==="json")try{return JSON.parse(t),!0}catch(n){return c(`[${s}] Invalid JSON for attribute "${o}": ${n.message}. Fallback: ${e.fallback}.`),!1}return!0}function ee(e){const t={};for(const o in e){const i=e[o];i&&i.effect&&(t[o]=i.effect)}return Object.keys(t).length?t:null}function te(e){if(e.onAttrChange&&!e.declared)return null;const t=new Set;if(e.effects)for(const o in e.effects)t.add(o);if(e.onAttrChange&&e.declared)for(const o of e.declared)t.add(o);return t}function E(e,t,o,i){if(e.nodeType!==1)return;const s=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",r=Array.from(e.querySelectorAll(s));e.matches&&e.matches(s)&&r.push(e);for(const c of r)if(!c[o]){window.lnCore._persistSink&&c.hasAttribute("data-ln-persist")&&window.lnCore._persistSink(c,t);try{c[o]=new i(c)}catch(n){console.error("["+o+"] init failed",c,n)}}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._bootHolds=window.lnCore._bootHolds||0,window.lnCore._bootQueue=window.lnCore._bootQueue||[]);function ne(){return typeof window<"u"&&window.lnCore&&window.lnCore._bootHolds||0}function oe(e){typeof window<"u"?(window.lnCore=window.lnCore||{},window.lnCore._bootHolds=window.lnCore._bootHolds||0,window.lnCore._bootQueue=window.lnCore._bootQueue||[],window.lnCore._bootHolds>0?window.lnCore._bootQueue.push(e):setTimeout(e,0)):e()}function B(){window.lnCore=window.lnCore||{};const e=window.lnCore._attrRegistry=window.lnCore._attrRegistry||{byAttr:new Map,reactive:[],persist:[]};return e.byReactive=e.byReactive||new Map,e.reactiveWildcard=e.reactiveWildcard||[],e}function re(e){const t=B(),o=e.observed||[];for(let i=0;i<o.length;i++){const l=o[i];t.byAttr.has(l)||t.byAttr.set(l,[]),t.byAttr.get(l).push(e)}if(t.byDeclaredAttr=t.byDeclaredAttr||new Map,e.attributes)for(const i in e.attributes){t.byDeclaredAttr.has(i)||t.byDeclaredAttr.set(i,[]);const l=t.byDeclaredAttr.get(i);l.some(s=>s.componentTag===e.componentTag)||l.push({spec:e.attributes[i],componentTag:e.componentTag})}if(e.onAttrChange||e.effects){t.reactive.push(e);const i=te(e);if(i===null)t.reactiveWildcard.push(e);else for(const l of i)t.byReactive.has(l)||t.byReactive.set(l,[]),t.byReactive.get(l).push(e)}e.persist&&t.persist.push(e)}function M(e,t,o,i){for(let l=0;l<e.length;l++){const s=e[l];if(!t[s.attribute])continue;const r=s.effects&&s.effects[o];r?r(t,o,i):s.onAttrChange&&(!s.declared||s.declared.has(o))&&s.onAttrChange(t,o,i)}}function ie(e){const t=e.target,o=e.attributeName;if(e.oldValue===t.getAttribute(o))return;const i=B(),l=i.byAttr.get(o);if(D()&&i.byDeclaredAttr&&i.byDeclaredAttr.has(o)&&A(t)){const s=i.byDeclaredAttr.get(o),r=t.getAttribute(o);for(let c=0;c<s.length;c++)q(s[c].spec,r,o,s[c].componentTag,t)}if(window.lnCore._debugSink&&(o.indexOf("data-ln-")===0||l)&&window.lnCore._debugSink("attr",o,t,{oldValue:e.oldValue,newValue:t.getAttribute(o)}),o.indexOf("data-ln-")===0){const s=i.byReactive.get(o);s&&M(s,t,o,e.oldValue),i.reactiveWildcard.length&&M(i.reactiveWildcard,t,o,e.oldValue)}if(l)for(let s=0;s<l.length;s++){const r=l[s];if(r.handler){r.handler(t,o,e.oldValue);continue}r.onAttributeChange&&t[r.attribute]?r.onAttributeChange(t,o):(E(t,r.selector,r.attribute,r.ComponentFn),r.onInit&&r.onInit(t))}}function le(){window.lnCore=window.lnCore||{},!window.lnCore._attrObserverBound&&(window.lnCore._attrObserverBound=!0,_(function(){new MutationObserver(function(t){for(let o=0;o<t.length;o++)try{ie(t[o])}catch(i){console.error("[ln-core] mutation handler failed",t[o].target,i)}}).observe(document.body,{attributes:!0,subtree:!0,attributeOldValue:!0})},"ln-core"))}function j(){return window.lnCore=window.lnCore||{},window.lnCore._lifecycleRegistry=window.lnCore._lifecycleRegistry||[]}function se(e){const t=j();if(t.length){if(e.target)for(let o=0;o<t.length;o++){const i=t[o];if(i.onSubtreeChange){const l=i.query,s=e.target.nodeType===1?e.target.matches(l)?e.target:e.target.closest(l):e.target.parentElement?e.target.parentElement.closest(l):null;s&&i.onSubtreeChange(s,e)}}for(let o=0;o<e.addedNodes.length;o++){const i=e.addedNodes[o];if(i.nodeType===1)for(let l=0;l<t.length;l++){const s=t[l];E(i,s.selector,s.attribute,s.ComponentFn),s.onInit&&s.onInit(i)}}for(let o=0;o<e.removedNodes.length;o++){const i=e.removedNodes[o];if(i.nodeType===1)for(let l=0;l<t.length;l++){const s=t[l],r=s.query,c=Array.from(i.querySelectorAll(r));i.matches&&i.matches(r)&&c.push(i);for(let n=0;n<c.length;n++){const a=c[n];if(!document.contains(a)){const d=a[s.attribute];if(d&&typeof d.destroy=="function")try{d.destroy()}catch(u){console.error("["+s.attribute+"] destroy failed",a,u)}delete a[s.attribute]}}}}}}function ce(){window.lnCore=window.lnCore||{},!window.lnCore._lifecycleObserverBound&&(window.lnCore._lifecycleObserverBound=!0,_(function(){new MutationObserver(function(t){for(let o=0;o<t.length;o++){const i=t[o];if(i.type==="childList")try{se(i)}catch(l){console.error("[ln-core] lifecycle handler failed",i.target,l)}}}).observe(document.body,{childList:!0,subtree:!0})},"ln-core"))}function ae(e,t,o,i,l={}){const s=l.extraAttributes||[],r=l.onAttributeChange||null,c=l.onSubtreeChange||null,n=l.onInit||null,a=l.onAttrChange||null,d=l.effects||null,u=l.attributes||null,h=u?ee(u):d,m=u?new Set(Object.keys(u)):null,O=l.persist||null;function p(g){const b=g||document.body;if(E(b,e,t,o),u&&D())for(const y in u){const _e=u[y],L=Array.from(b.querySelectorAll("["+y+"]"));b.matches&&b.matches("["+y+"]")&&L.push(b);for(let T=0;T<L.length;T++){const I=L[T];A(I)&&q(_e,I.getAttribute(y),y,i,I)}}n&&n(b)}const C=[];if(e.indexOf("[")!==-1){const g=/\[([\w-]+)/g;let b;for(;(b=g.exec(e))!==null;)C.push(b[1])}else C.push(e);re({selector:e,attribute:t,componentTag:i,attributes:u,ComponentFn:o,onInit:n,observed:C.concat(s),onAttributeChange:r,onAttrChange:a,effects:h,declared:m,persist:O}),le();const f=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]";j().push({selector:e,attribute:t,ComponentFn:o,onInit:n,onSubtreeChange:c,query:f}),ce(),window[t]=p;function x(){ne()>0?oe(function(){p(document.body)}):p(document.body)}return document.readyState==="loading"?document.addEventListener("DOMContentLoaded",x):x(),p}function W(e){return(e||"").replace(/^#/,"")}function S(e){const t=e===void 0?location.hash:e,o={},i=W(t);if(!i)return o;const l=i.split("&");for(let s=0;s<l.length;s++){const r=l[s];if(!r)continue;const c=r.indexOf(":"),n=c>-1?r.slice(0,c):r,a=c>-1?r.slice(c+1):"";if(n)try{o[n]=decodeURIComponent(a)}catch{o[n]=a}}return o}function ue(e){if(!e)return null;const t=S();return e in t?t[e]:null}function de(e,t){if(!e)return;const o=S();t==null?delete o[e]:o[e]=String(t);const l=Object.keys(o).map(function(s){const r=o[s];return r===""?s:s+":"+encodeURIComponent(r)}).join("&");W(location.hash)!==l&&(location.hash=l)}function fe(e){return e.button===1||e.ctrlKey||e.metaKey||e.shiftKey?!1:(e.preventDefault(),!0)}function he(e,t){if(!e||!e.hasAttribute("data-ln-hash"))return null;const o=e.getAttribute("data-ln-hash");if(o&&o.trim()!=="")return o.trim();const i=e.getAttribute("data-ln-sort")||e.getAttribute("data-ln-search-for")||e.getAttribute("data-ln-search")||e.getAttribute("data-ln-filter")||e.id;return i?t?i+"-"+t:i:t||null}function we(e,t){return!t||t==="none"||e===null||e===void 0?null:String(e)+"."+t}function be(e){return!e||typeof e!="string"?null:e.endsWith(".asc")?{fieldOrColumn:e.slice(0,-4),direction:"asc"}:e.endsWith(".desc")?{fieldOrColumn:e.slice(0,-5),direction:"desc"}:null}function pe(e,t){return!e||!Array.isArray(t)||t.length===0?null:e+":"+t.map(encodeURIComponent).join(",")}function ge(e){if(!e||typeof e!="string")return null;const t=e.indexOf(":");if(t===-1)return null;const o=e.slice(0,t),i=e.slice(t+1),l=i?i.split(",").map(function(s){try{return decodeURIComponent(s)}catch{return s}}).filter(Boolean):[];return{key:o,values:l}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=S,window.lnCore.hashGet=ue,window.lnCore.hashSet=de,window.lnCore.hashLinkClick=fe,window.lnCore.resolveHashNamespace=he,window.lnCore.hashSortEncode=we,window.lnCore.hashSortDecode=be,window.lnCore.hashFilterEncode=pe,window.lnCore.hashFilterDecode=ge);const Ce="ln-autosave:",me=1e3;function ye(e,t){return t?Ce+(e||"")+":"+t:null}function ve(e,t=me){if(e==null)return 0;if(e==="")return t;const o=parseInt(String(e),10);return isNaN(o)||o<0?t:o}(function(){const e="data-ln-autosave",t="lnAutosave",o="data-ln-autosave-clear",i="data-ln-autosave-debounce-input",l='[data-ln-autosave-exclude], input[type="password"]';if(window[t]!==void 0)return;const s={"data-ln-autosave":{type:"string",description:"Form autosave storage key identifier"},"data-ln-autosave-debounce-input":{type:"integer",fallback:500,min:0,description:"Debounce delay in milliseconds before saving on input events"},"data-ln-autosave-clear":{type:"marker",description:"Designates a button that clears saved form data from localStorage"},"data-ln-autosave-exclude":{type:"marker",description:"Excludes form control from autosave serialization"}};function r(n){const a=n.tagName;return a==="INPUT"||a==="TEXTAREA"||a==="SELECT"}function c(n){const d=n.getAttribute(e)||n.id,u=ye(window.location.pathname,d);if(!u){console.warn("ln-autosave: form needs an id or data-ln-autosave value",n);return}this.dom=n,this.key=u;let h=null;function m(){const w=H(n,{exclude:l});try{localStorage.setItem(u,JSON.stringify(w))}catch{return}v(n,"ln-autosave:saved",{target:n,data:w})}function O(){let w;try{w=localStorage.getItem(u)}catch{return}if(!w)return;let f;try{f=JSON.parse(w)}catch{return}if(U(n,"ln-autosave:before-restore",{target:n,data:f}).defaultPrevented)return;const g=K(n,f);for(let b=0;b<g.length;b++)g[b].dispatchEvent(new Event("input",{bubbles:!0})),g[b].dispatchEvent(new Event("change",{bubbles:!0}));v(n,"ln-autosave:restored",{target:n,data:f})}function p(){try{localStorage.removeItem(u)}catch{return}v(n,"ln-autosave:cleared",{target:n})}this._onFocusout=function(w){const f=w.target;r(f)&&f.name&&!f.matches(l)&&m()},this._onChange=function(w){const f=w.target;r(f)&&f.name&&!f.matches(l)&&m()},this._onSubmit=function(){p()},this._onReset=function(){p()},this._onClearClick=function(w){w.target.closest("["+o+"]")&&p()},n.addEventListener("focusout",this._onFocusout),n.addEventListener("change",this._onChange),n.addEventListener("submit",this._onSubmit),n.addEventListener("reset",this._onReset),n.addEventListener("click",this._onClearClick);const C=ve(n.getAttribute(i));return C>0&&(this._onInput=function(w){const f=w.target;!r(f)||!f.name||f.matches(l)||(h!==null&&clearTimeout(h),h=setTimeout(m,C))},n.addEventListener("input",this._onInput)),this._getInputTimer=function(){return h},O(),this}c.prototype.destroy=function(){if(this.dom[t]){if(this.dom.removeEventListener("focusout",this._onFocusout),this.dom.removeEventListener("change",this._onChange),this.dom.removeEventListener("submit",this._onSubmit),this.dom.removeEventListener("reset",this._onReset),this.dom.removeEventListener("click",this._onClearClick),this._onInput){this.dom.removeEventListener("input",this._onInput);const n=this._getInputTimer();n!==null&&clearTimeout(n)}delete this.dom[t]}},ae(e,t,c,"ln-autosave",{attributes:s})})()})();
@@ -0,0 +1,48 @@
1
+ {
2
+ "$comment": "This file is partly generated. The $comment, component, generator, and each attribute's direction, sources, type, values, fallback, and description keys are owned by the generator and synchronized from JS source code. All other keys are preserved and safe to hand-author. Regenerate with `npm run sync:ln-schemas`.",
3
+ "component": "ln-autosave",
4
+ "generator": "scripts/sync-ln-schemas.mjs",
5
+ "attributes": {
6
+ "data-ln-autosave": {
7
+ "direction": "author",
8
+ "sources": [
9
+ "ln-autosave-dev.scss",
10
+ "src/ln-autosave.js"
11
+ ],
12
+ "type": "string",
13
+ "description": "Form autosave storage key identifier"
14
+ },
15
+ "data-ln-autosave-clear": {
16
+ "direction": "author",
17
+ "sources": [
18
+ "src/ln-autosave.js"
19
+ ],
20
+ "type": "marker",
21
+ "description": "Designates a button that clears saved form data from localStorage"
22
+ },
23
+ "data-ln-autosave-debounce-input": {
24
+ "direction": "author",
25
+ "sources": [
26
+ "src/ln-autosave.js"
27
+ ],
28
+ "type": "integer",
29
+ "fallback": 500,
30
+ "min": 0,
31
+ "description": "Debounce delay in milliseconds before saving on input events"
32
+ },
33
+ "data-ln-autosave-exclude": {
34
+ "direction": null,
35
+ "sources": [
36
+ "src/ln-autosave.js"
37
+ ],
38
+ "type": "marker",
39
+ "description": "Excludes form control from autosave serialization"
40
+ },
41
+ "data-ln-debug": {
42
+ "direction": null,
43
+ "sources": [
44
+ "ln-autosave-dev.scss"
45
+ ]
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,34 @@
1
+ export const STORAGE_PREFIX = 'ln-autosave:';
2
+ export const DEFAULT_DEBOUNCE_MS = 1000;
3
+
4
+ /**
5
+ * Builds a deterministic storage key for autosave persistence.
6
+ * @param {string} pathname
7
+ * @param {string} identifier
8
+ * @returns {string|null}
9
+ */
10
+ export function buildAutosaveKey(pathname, identifier) {
11
+ if (!identifier) return null;
12
+ const path = pathname || '';
13
+ return STORAGE_PREFIX + path + ':' + identifier;
14
+ }
15
+
16
+ /**
17
+ * Parses and sanitizes the debounce milliseconds attribute value for ln-autosave.
18
+ *
19
+ * Semantics:
20
+ * - Returns `0` if the attribute is absent (null/undefined), signaling that input debouncing is disabled (no listener).
21
+ * - Returns `fallbackMs` (default 1000) if the attribute is present as boolean/empty string `""` or contains an invalid value.
22
+ * - Returns the parsed positive integer if a valid numeric string is provided.
23
+ *
24
+ * @param {unknown} rawValue Raw DOM attribute value (e.g. data-ln-autosave-debounce-input)
25
+ * @param {number} [fallbackMs=1000] Fallback debounce duration in ms
26
+ * @returns {number} Milliseconds to debounce, or 0 if disabled
27
+ */
28
+ export function parseAutosaveDebounce(rawValue, fallbackMs = DEFAULT_DEBOUNCE_MS) {
29
+ if (rawValue === null || rawValue === undefined) return 0;
30
+ if (rawValue === '') return fallbackMs;
31
+ const parsed = parseInt(String(rawValue), 10);
32
+ if (isNaN(parsed) || parsed < 0) return fallbackMs;
33
+ return parsed;
34
+ }
@@ -1,19 +1,35 @@
1
- import { dispatch, dispatchCancelable, serializeForm, populateForm, registerComponent } from '../../ln-core';
1
+ import { dispatch, dispatchCancelable, populateForm, registerComponent, serializeForm } from '../../ln-core';
2
+ import { buildAutosaveKey, parseAutosaveDebounce } from './autosave-model.js';
2
3
 
3
4
  (function () {
4
5
  const DOM_SELECTOR = 'data-ln-autosave';
5
6
  const DOM_ATTRIBUTE = 'lnAutosave';
6
7
  const CLEAR_SELECTOR = 'data-ln-autosave-clear';
7
8
  const DEBOUNCE_SELECTOR = 'data-ln-autosave-debounce-input';
8
- const STORAGE_PREFIX = 'ln-autosave:';
9
- const DEFAULT_DEBOUNCE_MS = 1000;
9
+ const EXCLUDE_SELECTOR = '[data-ln-autosave-exclude], input[type="password"]';
10
10
 
11
11
  if (window[DOM_ATTRIBUTE] !== undefined) return;
12
12
 
13
+ // ─── Attribute Contract (SSOT) ──────────────────────────
14
+ const ATTRIBUTES = {
15
+ 'data-ln-autosave': { type: 'string', description: 'Form autosave storage key identifier' },
16
+ 'data-ln-autosave-debounce-input': { type: 'integer', fallback: 500, min: 0, description: 'Debounce delay in milliseconds before saving on input events' },
17
+ 'data-ln-autosave-clear': { type: 'marker', description: 'Designates a button that clears saved form data from localStorage' },
18
+ 'data-ln-autosave-exclude': { type: 'marker', description: 'Excludes form control from autosave serialization' }
19
+ };
20
+
21
+ function _isFormField(el) {
22
+ const tag = el.tagName;
23
+ return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
24
+ }
25
+
13
26
  // ─── Component ─────────────────────────────────────────────
14
27
 
15
28
  function _component(form) {
16
- const key = _getStorageKey(form);
29
+ const value = form.getAttribute(DOM_SELECTOR);
30
+ const identifier = value || form.id;
31
+ const key = buildAutosaveKey(window.location.pathname, identifier);
32
+
17
33
  if (!key) {
18
34
  console.warn('ln-autosave: form needs an id or data-ln-autosave value', form);
19
35
  return;
@@ -24,20 +40,30 @@ import { dispatch, dispatchCancelable, serializeForm, populateForm, registerComp
24
40
 
25
41
  let inputTimer = null;
26
42
 
27
- // Closure-scoped helpers (NOT on prototype — internal only)
28
43
  function _save() {
29
- const data = serializeForm(form, { exclude: '[data-ln-autosave-exclude]' });
30
- try { localStorage.setItem(key, JSON.stringify(data)); }
31
- catch (e) { return; }
44
+ const data = serializeForm(form, { exclude: EXCLUDE_SELECTOR });
45
+ try {
46
+ localStorage.setItem(key, JSON.stringify(data));
47
+ } catch (_) {
48
+ return;
49
+ }
32
50
  dispatch(form, 'ln-autosave:saved', { target: form, data: data });
33
51
  }
34
52
 
35
53
  function _restore() {
36
54
  let raw;
37
- try { raw = localStorage.getItem(key); } catch (e) { return; }
55
+ try {
56
+ raw = localStorage.getItem(key);
57
+ } catch (_) {
58
+ return;
59
+ }
38
60
  if (!raw) return;
39
61
  let data;
40
- try { data = JSON.parse(raw); } catch (e) { return; }
62
+ try {
63
+ data = JSON.parse(raw);
64
+ } catch (_) {
65
+ return;
66
+ }
41
67
  const before = dispatchCancelable(form, 'ln-autosave:before-restore', { target: form, data: data });
42
68
  if (before.defaultPrevented) return;
43
69
  const restored = populateForm(form, data);
@@ -49,24 +75,31 @@ import { dispatch, dispatchCancelable, serializeForm, populateForm, registerComp
49
75
  }
50
76
 
51
77
  function _clear() {
52
- try { localStorage.removeItem(key); } catch (e) { return; }
78
+ try {
79
+ localStorage.removeItem(key);
80
+ } catch (_) {
81
+ return;
82
+ }
53
83
  dispatch(form, 'ln-autosave:cleared', { target: form });
54
84
  }
55
85
 
56
- // Listener handlers (held on `this` for symmetric removal in destroy)
57
86
  this._onFocusout = function (e) {
58
87
  const el = e.target;
59
- if (_isFormField(el) && el.name && !el.hasAttribute('data-ln-autosave-exclude')) _save();
88
+ if (_isFormField(el) && el.name && !el.matches(EXCLUDE_SELECTOR)) _save();
60
89
  };
61
90
 
62
91
  this._onChange = function (e) {
63
92
  const el = e.target;
64
- if (_isFormField(el) && el.name && !el.hasAttribute('data-ln-autosave-exclude')) _save();
93
+ if (_isFormField(el) && el.name && !el.matches(EXCLUDE_SELECTOR)) _save();
65
94
  };
66
95
 
67
- this._onSubmit = function () { _clear(); };
96
+ this._onSubmit = function () {
97
+ _clear();
98
+ };
68
99
 
69
- this._onReset = function () { _clear(); };
100
+ this._onReset = function () {
101
+ _clear();
102
+ };
70
103
 
71
104
  this._onClearClick = function (e) {
72
105
  const btn = e.target.closest('[' + CLEAR_SELECTOR + ']');
@@ -74,74 +107,48 @@ import { dispatch, dispatchCancelable, serializeForm, populateForm, registerComp
74
107
  };
75
108
 
76
109
  form.addEventListener('focusout', this._onFocusout);
77
- form.addEventListener('change', this._onChange);
78
- form.addEventListener('submit', this._onSubmit);
79
- form.addEventListener('reset', this._onReset);
80
- form.addEventListener('click', this._onClearClick);
110
+ form.addEventListener('change', this._onChange);
111
+ form.addEventListener('submit', this._onSubmit);
112
+ form.addEventListener('reset', this._onReset);
113
+ form.addEventListener('click', this._onClearClick);
81
114
 
82
- // Optional debounced input listener (opt-in via data-ln-autosave-debounce-input)
83
- const debounceMs = _resolveDebounceMs(form);
115
+ const debounceMs = parseAutosaveDebounce(form.getAttribute(DEBOUNCE_SELECTOR));
84
116
  if (debounceMs > 0) {
85
117
  this._onInput = function (e) {
86
118
  const el = e.target;
87
- if (!_isFormField(el) || !el.name || el.hasAttribute('data-ln-autosave-exclude')) return;
119
+ if (!_isFormField(el) || !el.name || el.matches(EXCLUDE_SELECTOR)) return;
88
120
  if (inputTimer !== null) clearTimeout(inputTimer);
89
121
  inputTimer = setTimeout(_save, debounceMs);
90
122
  };
91
123
  form.addEventListener('input', this._onInput);
92
124
  }
93
125
 
94
- // Thunk so destroy() can reach the closure-scoped inputTimer
95
- this._getInputTimer = function () { return inputTimer; };
126
+ this._getInputTimer = function () {
127
+ return inputTimer;
128
+ };
96
129
 
97
130
  _restore();
98
-
99
131
  return this;
100
132
  }
101
133
 
102
134
  _component.prototype.destroy = function () {
103
135
  if (!this.dom[DOM_ATTRIBUTE]) return;
104
136
  this.dom.removeEventListener('focusout', this._onFocusout);
105
- this.dom.removeEventListener('change', this._onChange);
106
- this.dom.removeEventListener('submit', this._onSubmit);
107
- this.dom.removeEventListener('reset', this._onReset);
108
- this.dom.removeEventListener('click', this._onClearClick);
137
+ this.dom.removeEventListener('change', this._onChange);
138
+ this.dom.removeEventListener('submit', this._onSubmit);
139
+ this.dom.removeEventListener('reset', this._onReset);
140
+ this.dom.removeEventListener('click', this._onClearClick);
109
141
  if (this._onInput) {
110
142
  this.dom.removeEventListener('input', this._onInput);
111
143
  const t = this._getInputTimer();
112
144
  if (t !== null) clearTimeout(t);
113
145
  }
114
- dispatch(this.dom, 'ln-autosave:destroyed', { target: this.dom });
115
146
  delete this.dom[DOM_ATTRIBUTE];
116
147
  };
117
148
 
118
- // ─── Helpers ───────────────────────────────────────────────
119
-
120
- function _getStorageKey(form) {
121
- const value = form.getAttribute(DOM_SELECTOR);
122
- const identifier = value || form.id;
123
- if (!identifier) return null;
124
- return STORAGE_PREFIX + window.location.pathname + ':' + identifier;
125
- }
126
-
127
- function _isFormField(el) {
128
- const tag = el.tagName;
129
- return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
130
- }
131
-
132
- function _resolveDebounceMs(form) {
133
- if (!form.hasAttribute(DEBOUNCE_SELECTOR)) return 0;
134
- const raw = form.getAttribute(DEBOUNCE_SELECTOR);
135
- if (raw === '' || raw === null) return DEFAULT_DEBOUNCE_MS;
136
- const ms = parseInt(raw, 10);
137
- if (isNaN(ms) || ms < 0) {
138
- console.warn('ln-autosave: invalid debounce value, using default', form);
139
- return DEFAULT_DEBOUNCE_MS;
140
- }
141
- return ms;
142
- }
143
-
144
149
  // ─── Init ──────────────────────────────────────────────────
145
150
 
146
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-autosave');
151
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-autosave', {
152
+ attributes: ATTRIBUTES
153
+ });
147
154
  })();