@lo-tek/interfaces 0.0.0 → 0.38.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 (456) hide show
  1. package/CHANGELOG.md +3389 -0
  2. package/LICENSE +21 -0
  3. package/README.md +129 -1
  4. package/agent/astro.md +400 -0
  5. package/agent/bundles.md +429 -0
  6. package/agent/datastar.md +409 -0
  7. package/agent/html.md +388 -0
  8. package/agent/react.md +440 -0
  9. package/agent/templates.md +472 -0
  10. package/core/lotek-core.wasm +0 -0
  11. package/core/lotek.wasm +0 -0
  12. package/dist/cli.d.ts +12 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +14 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/components/css/accordion/accordion.d.ts +37 -0
  17. package/dist/components/css/accordion/accordion.d.ts.map +1 -0
  18. package/dist/components/css/accordion/accordion.js +31 -0
  19. package/dist/components/css/accordion/accordion.js.map +1 -0
  20. package/dist/components/css/alert/alert.d.ts +35 -0
  21. package/dist/components/css/alert/alert.d.ts.map +1 -0
  22. package/dist/components/css/alert/alert.js +29 -0
  23. package/dist/components/css/alert/alert.js.map +1 -0
  24. package/dist/components/css/avatar/avatar.d.ts +40 -0
  25. package/dist/components/css/avatar/avatar.d.ts.map +1 -0
  26. package/dist/components/css/avatar/avatar.js +30 -0
  27. package/dist/components/css/avatar/avatar.js.map +1 -0
  28. package/dist/components/css/badge/badge.d.ts +24 -0
  29. package/dist/components/css/badge/badge.d.ts.map +1 -0
  30. package/dist/components/css/badge/badge.js +24 -0
  31. package/dist/components/css/badge/badge.js.map +1 -0
  32. package/dist/components/css/breadcrumbs/breadcrumbs.d.ts +26 -0
  33. package/dist/components/css/breadcrumbs/breadcrumbs.d.ts.map +1 -0
  34. package/dist/components/css/breadcrumbs/breadcrumbs.js +24 -0
  35. package/dist/components/css/breadcrumbs/breadcrumbs.js.map +1 -0
  36. package/dist/components/css/button/button.d.ts +26 -0
  37. package/dist/components/css/button/button.d.ts.map +1 -0
  38. package/dist/components/css/button/button.js +25 -0
  39. package/dist/components/css/button/button.js.map +1 -0
  40. package/dist/components/css/card/card.d.ts +36 -0
  41. package/dist/components/css/card/card.d.ts.map +1 -0
  42. package/dist/components/css/card/card.js +28 -0
  43. package/dist/components/css/card/card.js.map +1 -0
  44. package/dist/components/css/checkbox/checkbox.d.ts +32 -0
  45. package/dist/components/css/checkbox/checkbox.d.ts.map +1 -0
  46. package/dist/components/css/checkbox/checkbox.js +27 -0
  47. package/dist/components/css/checkbox/checkbox.js.map +1 -0
  48. package/dist/components/css/divider/divider.d.ts +29 -0
  49. package/dist/components/css/divider/divider.d.ts.map +1 -0
  50. package/dist/components/css/divider/divider.js +23 -0
  51. package/dist/components/css/divider/divider.js.map +1 -0
  52. package/dist/components/css/error-text/error-text.d.ts +24 -0
  53. package/dist/components/css/error-text/error-text.d.ts.map +1 -0
  54. package/dist/components/css/error-text/error-text.js +18 -0
  55. package/dist/components/css/error-text/error-text.js.map +1 -0
  56. package/dist/components/css/fieldset/fieldset.d.ts +55 -0
  57. package/dist/components/css/fieldset/fieldset.d.ts.map +1 -0
  58. package/dist/components/css/fieldset/fieldset.js +39 -0
  59. package/dist/components/css/fieldset/fieldset.js.map +1 -0
  60. package/dist/components/css/file-upload/file-upload.d.ts +41 -0
  61. package/dist/components/css/file-upload/file-upload.d.ts.map +1 -0
  62. package/dist/components/css/file-upload/file-upload.js +32 -0
  63. package/dist/components/css/file-upload/file-upload.js.map +1 -0
  64. package/dist/components/css/heading/heading.d.ts +29 -0
  65. package/dist/components/css/heading/heading.d.ts.map +1 -0
  66. package/dist/components/css/heading/heading.js +23 -0
  67. package/dist/components/css/heading/heading.js.map +1 -0
  68. package/dist/components/css/help-text/help-text.d.ts +24 -0
  69. package/dist/components/css/help-text/help-text.d.ts.map +1 -0
  70. package/dist/components/css/help-text/help-text.js +23 -0
  71. package/dist/components/css/help-text/help-text.js.map +1 -0
  72. package/dist/components/css/icon/icon.d.ts +34 -0
  73. package/dist/components/css/icon/icon.d.ts.map +1 -0
  74. package/dist/components/css/icon/icon.js +25 -0
  75. package/dist/components/css/icon/icon.js.map +1 -0
  76. package/dist/components/css/input/input.d.ts +40 -0
  77. package/dist/components/css/input/input.d.ts.map +1 -0
  78. package/dist/components/css/input/input.js +45 -0
  79. package/dist/components/css/input/input.js.map +1 -0
  80. package/dist/components/css/label/label.d.ts +33 -0
  81. package/dist/components/css/label/label.d.ts.map +1 -0
  82. package/dist/components/css/label/label.js +23 -0
  83. package/dist/components/css/label/label.js.map +1 -0
  84. package/dist/components/css/link/link.d.ts +28 -0
  85. package/dist/components/css/link/link.d.ts.map +1 -0
  86. package/dist/components/css/link/link.js +20 -0
  87. package/dist/components/css/link/link.js.map +1 -0
  88. package/dist/components/css/list/list.d.ts +31 -0
  89. package/dist/components/css/list/list.d.ts.map +1 -0
  90. package/dist/components/css/list/list.js +25 -0
  91. package/dist/components/css/list/list.js.map +1 -0
  92. package/dist/components/css/pagination/pagination.d.ts +27 -0
  93. package/dist/components/css/pagination/pagination.d.ts.map +1 -0
  94. package/dist/components/css/pagination/pagination.js +25 -0
  95. package/dist/components/css/pagination/pagination.js.map +1 -0
  96. package/dist/components/css/paragraph/paragraph.d.ts +36 -0
  97. package/dist/components/css/paragraph/paragraph.d.ts.map +1 -0
  98. package/dist/components/css/paragraph/paragraph.js +26 -0
  99. package/dist/components/css/paragraph/paragraph.js.map +1 -0
  100. package/dist/components/css/progress/progress.d.ts +53 -0
  101. package/dist/components/css/progress/progress.d.ts.map +1 -0
  102. package/dist/components/css/progress/progress.js +38 -0
  103. package/dist/components/css/progress/progress.js.map +1 -0
  104. package/dist/components/css/radio/radio.d.ts +32 -0
  105. package/dist/components/css/radio/radio.d.ts.map +1 -0
  106. package/dist/components/css/radio/radio.js +27 -0
  107. package/dist/components/css/radio/radio.js.map +1 -0
  108. package/dist/components/css/search/search.d.ts +34 -0
  109. package/dist/components/css/search/search.d.ts.map +1 -0
  110. package/dist/components/css/search/search.js +32 -0
  111. package/dist/components/css/search/search.js.map +1 -0
  112. package/dist/components/css/select/select.d.ts +40 -0
  113. package/dist/components/css/select/select.d.ts.map +1 -0
  114. package/dist/components/css/select/select.js +31 -0
  115. package/dist/components/css/select/select.js.map +1 -0
  116. package/dist/components/css/shared.d.ts +90 -0
  117. package/dist/components/css/shared.d.ts.map +1 -0
  118. package/dist/components/css/shared.js +167 -0
  119. package/dist/components/css/shared.js.map +1 -0
  120. package/dist/components/css/skeleton/skeleton.d.ts +29 -0
  121. package/dist/components/css/skeleton/skeleton.d.ts.map +1 -0
  122. package/dist/components/css/skeleton/skeleton.js +23 -0
  123. package/dist/components/css/skeleton/skeleton.js.map +1 -0
  124. package/dist/components/css/skip-link/skip-link.d.ts +18 -0
  125. package/dist/components/css/skip-link/skip-link.d.ts.map +1 -0
  126. package/dist/components/css/skip-link/skip-link.js +16 -0
  127. package/dist/components/css/skip-link/skip-link.js.map +1 -0
  128. package/dist/components/css/spinner/spinner.d.ts +33 -0
  129. package/dist/components/css/spinner/spinner.d.ts.map +1 -0
  130. package/dist/components/css/spinner/spinner.js +25 -0
  131. package/dist/components/css/spinner/spinner.js.map +1 -0
  132. package/dist/components/css/sr-only/sr-only.d.ts +17 -0
  133. package/dist/components/css/sr-only/sr-only.d.ts.map +1 -0
  134. package/dist/components/css/sr-only/sr-only.js +15 -0
  135. package/dist/components/css/sr-only/sr-only.js.map +1 -0
  136. package/dist/components/css/switch/switch.d.ts +37 -0
  137. package/dist/components/css/switch/switch.d.ts.map +1 -0
  138. package/dist/components/css/switch/switch.js +29 -0
  139. package/dist/components/css/switch/switch.js.map +1 -0
  140. package/dist/components/css/table/table.d.ts +76 -0
  141. package/dist/components/css/table/table.d.ts.map +1 -0
  142. package/dist/components/css/table/table.js +59 -0
  143. package/dist/components/css/table/table.js.map +1 -0
  144. package/dist/components/css/tag/tag.d.ts +35 -0
  145. package/dist/components/css/tag/tag.d.ts.map +1 -0
  146. package/dist/components/css/tag/tag.js +27 -0
  147. package/dist/components/css/tag/tag.js.map +1 -0
  148. package/dist/components/css/textarea/textarea.d.ts +23 -0
  149. package/dist/components/css/textarea/textarea.d.ts.map +1 -0
  150. package/dist/components/css/textarea/textarea.js +18 -0
  151. package/dist/components/css/textarea/textarea.js.map +1 -0
  152. package/dist/components/css/toggle-group/toggle-group.d.ts +31 -0
  153. package/dist/components/css/toggle-group/toggle-group.d.ts.map +1 -0
  154. package/dist/components/css/toggle-group/toggle-group.js +29 -0
  155. package/dist/components/css/toggle-group/toggle-group.js.map +1 -0
  156. package/dist/components/css/tooltip/tooltip.d.ts +31 -0
  157. package/dist/components/css/tooltip/tooltip.d.ts.map +1 -0
  158. package/dist/components/css/tooltip/tooltip.js +29 -0
  159. package/dist/components/css/tooltip/tooltip.js.map +1 -0
  160. package/dist/components/default-texts.d.ts +41 -0
  161. package/dist/components/default-texts.d.ts.map +1 -0
  162. package/dist/components/default-texts.js +41 -0
  163. package/dist/components/default-texts.js.map +1 -0
  164. package/dist/components/events.d.ts +77 -0
  165. package/dist/components/events.d.ts.map +1 -0
  166. package/dist/components/events.js +3 -0
  167. package/dist/components/events.js.map +1 -0
  168. package/dist/components/frame/dialog/dialog.d.ts +122 -0
  169. package/dist/components/frame/dialog/dialog.d.ts.map +1 -0
  170. package/dist/components/frame/dialog/dialog.js +112 -0
  171. package/dist/components/frame/dialog/dialog.js.map +1 -0
  172. package/dist/components/frame/dialog/lt-dialog.d.ts +104 -0
  173. package/dist/components/frame/dialog/lt-dialog.d.ts.map +1 -0
  174. package/dist/components/frame/dialog/lt-dialog.js +434 -0
  175. package/dist/components/frame/dialog/lt-dialog.js.map +1 -0
  176. package/dist/components/frame/error-summary/error-summary.d.ts +53 -0
  177. package/dist/components/frame/error-summary/error-summary.d.ts.map +1 -0
  178. package/dist/components/frame/error-summary/error-summary.js +54 -0
  179. package/dist/components/frame/error-summary/error-summary.js.map +1 -0
  180. package/dist/components/frame/error-summary/lt-error-summary.d.ts +93 -0
  181. package/dist/components/frame/error-summary/lt-error-summary.d.ts.map +1 -0
  182. package/dist/components/frame/error-summary/lt-error-summary.js +408 -0
  183. package/dist/components/frame/error-summary/lt-error-summary.js.map +1 -0
  184. package/dist/components/frame/field/field-core.d.ts +92 -0
  185. package/dist/components/frame/field/field-core.d.ts.map +1 -0
  186. package/dist/components/frame/field/field-core.js +63 -0
  187. package/dist/components/frame/field/field-core.js.map +1 -0
  188. package/dist/components/frame/field/lt-field.d.ts +173 -0
  189. package/dist/components/frame/field/lt-field.d.ts.map +1 -0
  190. package/dist/components/frame/field/lt-field.js +600 -0
  191. package/dist/components/frame/field/lt-field.js.map +1 -0
  192. package/dist/components/frame/popover/lt-popover.d.ts +136 -0
  193. package/dist/components/frame/popover/lt-popover.d.ts.map +1 -0
  194. package/dist/components/frame/popover/lt-popover.js +520 -0
  195. package/dist/components/frame/popover/lt-popover.js.map +1 -0
  196. package/dist/components/frame/popover/popover.d.ts +49 -0
  197. package/dist/components/frame/popover/popover.d.ts.map +1 -0
  198. package/dist/components/frame/popover/popover.js +64 -0
  199. package/dist/components/frame/popover/popover.js.map +1 -0
  200. package/dist/components/frame/session-timeout/lt-session-timeout.d.ts +198 -0
  201. package/dist/components/frame/session-timeout/lt-session-timeout.d.ts.map +1 -0
  202. package/dist/components/frame/session-timeout/lt-session-timeout.js +610 -0
  203. package/dist/components/frame/session-timeout/lt-session-timeout.js.map +1 -0
  204. package/dist/components/frame/session-timeout/session-timeout.d.ts +113 -0
  205. package/dist/components/frame/session-timeout/session-timeout.d.ts.map +1 -0
  206. package/dist/components/frame/session-timeout/session-timeout.js +101 -0
  207. package/dist/components/frame/session-timeout/session-timeout.js.map +1 -0
  208. package/dist/components/frame/suggestion/lt-suggestion.d.ts +367 -0
  209. package/dist/components/frame/suggestion/lt-suggestion.d.ts.map +1 -0
  210. package/dist/components/frame/suggestion/lt-suggestion.js +1171 -0
  211. package/dist/components/frame/suggestion/lt-suggestion.js.map +1 -0
  212. package/dist/components/frame/suggestion/suggestion.d.ts +143 -0
  213. package/dist/components/frame/suggestion/suggestion.d.ts.map +1 -0
  214. package/dist/components/frame/suggestion/suggestion.js +127 -0
  215. package/dist/components/frame/suggestion/suggestion.js.map +1 -0
  216. package/dist/components/frame/tabs/lt-tabs.d.ts +426 -0
  217. package/dist/components/frame/tabs/lt-tabs.d.ts.map +1 -0
  218. package/dist/components/frame/tabs/lt-tabs.js +1265 -0
  219. package/dist/components/frame/tabs/lt-tabs.js.map +1 -0
  220. package/dist/components/frame/tabs/tabs.d.ts +112 -0
  221. package/dist/components/frame/tabs/tabs.d.ts.map +1 -0
  222. package/dist/components/frame/tabs/tabs.js +107 -0
  223. package/dist/components/frame/tabs/tabs.js.map +1 -0
  224. package/dist/components/host-element.d.ts +184 -0
  225. package/dist/components/host-element.d.ts.map +1 -0
  226. package/dist/components/host-element.js +440 -0
  227. package/dist/components/host-element.js.map +1 -0
  228. package/dist/components/standalone/connection-status/connection-status.d.ts +32 -0
  229. package/dist/components/standalone/connection-status/connection-status.d.ts.map +1 -0
  230. package/dist/components/standalone/connection-status/connection-status.js +26 -0
  231. package/dist/components/standalone/connection-status/connection-status.js.map +1 -0
  232. package/dist/components/standalone/connection-status/lt-connection-status.d.ts +86 -0
  233. package/dist/components/standalone/connection-status/lt-connection-status.d.ts.map +1 -0
  234. package/dist/components/standalone/connection-status/lt-connection-status.js +240 -0
  235. package/dist/components/standalone/connection-status/lt-connection-status.js.map +1 -0
  236. package/dist/components/standalone/toast/lt-toast.d.ts +109 -0
  237. package/dist/components/standalone/toast/lt-toast.d.ts.map +1 -0
  238. package/dist/components/standalone/toast/lt-toast.js +256 -0
  239. package/dist/components/standalone/toast/lt-toast.js.map +1 -0
  240. package/dist/components/standalone/toast/toast.d.ts +52 -0
  241. package/dist/components/standalone/toast/toast.d.ts.map +1 -0
  242. package/dist/components/standalone/toast/toast.js +42 -0
  243. package/dist/components/standalone/toast/toast.js.map +1 -0
  244. package/dist/diagnostics/core.d.ts +171 -0
  245. package/dist/diagnostics/core.d.ts.map +1 -0
  246. package/dist/diagnostics/core.js +84 -0
  247. package/dist/diagnostics/core.js.map +1 -0
  248. package/dist/diagnostics/default-core.d.ts +10 -0
  249. package/dist/diagnostics/default-core.d.ts.map +1 -0
  250. package/dist/diagnostics/default-core.js +72 -0
  251. package/dist/diagnostics/default-core.js.map +1 -0
  252. package/dist/diagnostics/index.d.ts +84 -0
  253. package/dist/diagnostics/index.d.ts.map +1 -0
  254. package/dist/diagnostics/index.js +106 -0
  255. package/dist/diagnostics/index.js.map +1 -0
  256. package/dist/dom.d.ts +55 -0
  257. package/dist/dom.d.ts.map +1 -0
  258. package/dist/dom.js +162 -0
  259. package/dist/dom.js.map +1 -0
  260. package/dist/index.d.ts +97 -0
  261. package/dist/index.d.ts.map +1 -0
  262. package/dist/index.js +58 -0
  263. package/dist/index.js.map +1 -0
  264. package/dist/jsx/react.d.ts +180 -0
  265. package/dist/jsx/react.d.ts.map +1 -0
  266. package/dist/jsx/react.js +2 -0
  267. package/dist/jsx/react.js.map +1 -0
  268. package/dist/lotek.css +3617 -0
  269. package/dist/lt.d.ts +325 -0
  270. package/dist/lt.d.ts.map +1 -0
  271. package/dist/lt.js +183 -0
  272. package/dist/lt.js.map +1 -0
  273. package/dist/react.d.ts +494 -0
  274. package/dist/react.d.ts.map +1 -0
  275. package/dist/react.js +296 -0
  276. package/dist/react.js.map +1 -0
  277. package/dist/register.d.ts +17 -0
  278. package/dist/register.d.ts.map +1 -0
  279. package/dist/register.js +35 -0
  280. package/dist/register.js.map +1 -0
  281. package/dist/tokens/color.d.ts +43 -0
  282. package/dist/tokens/color.d.ts.map +1 -0
  283. package/dist/tokens/color.js +123 -0
  284. package/dist/tokens/color.js.map +1 -0
  285. package/dist/tokens/contract.d.ts +18 -0
  286. package/dist/tokens/contract.d.ts.map +1 -0
  287. package/dist/tokens/contract.js +18 -0
  288. package/dist/tokens/contract.js.map +1 -0
  289. package/dist/tokens/matrix-cells.d.ts +30 -0
  290. package/dist/tokens/matrix-cells.d.ts.map +1 -0
  291. package/dist/tokens/matrix-cells.js +273 -0
  292. package/dist/tokens/matrix-cells.js.map +1 -0
  293. package/dist/tokens/matrix.d.ts +18 -0
  294. package/dist/tokens/matrix.d.ts.map +1 -0
  295. package/dist/tokens/matrix.js +21 -0
  296. package/dist/tokens/matrix.js.map +1 -0
  297. package/dist/tokens/theme-check.d.ts +42 -0
  298. package/dist/tokens/theme-check.d.ts.map +1 -0
  299. package/dist/tokens/theme-check.js +10 -0
  300. package/dist/tokens/theme-check.js.map +1 -0
  301. package/dist/tokens/theme.d.ts +94 -0
  302. package/dist/tokens/theme.d.ts.map +1 -0
  303. package/dist/tokens/theme.js +14 -0
  304. package/dist/tokens/theme.js.map +1 -0
  305. package/dist/tokens/tokens.d.ts +122 -0
  306. package/dist/tokens/tokens.d.ts.map +1 -0
  307. package/dist/tokens/tokens.js +158 -0
  308. package/dist/tokens/tokens.js.map +1 -0
  309. package/dist/vocabulary/classes.d.ts +3 -0
  310. package/dist/vocabulary/classes.d.ts.map +1 -0
  311. package/dist/vocabulary/classes.js +1354 -0
  312. package/dist/vocabulary/classes.js.map +1 -0
  313. package/dist/vocabulary/elements.d.ts +3 -0
  314. package/dist/vocabulary/elements.d.ts.map +1 -0
  315. package/dist/vocabulary/elements.js +490 -0
  316. package/dist/vocabulary/elements.js.map +1 -0
  317. package/dist/vocabulary/types.d.ts +229 -0
  318. package/dist/vocabulary/types.d.ts.map +1 -0
  319. package/dist/vocabulary/types.js +11 -0
  320. package/dist/vocabulary/types.js.map +1 -0
  321. package/dist/wasi-host.d.ts +24 -0
  322. package/dist/wasi-host.d.ts.map +1 -0
  323. package/dist/wasi-host.js +140 -0
  324. package/dist/wasi-host.js.map +1 -0
  325. package/live-templates.xml +63 -0
  326. package/manifest/files.json +373 -0
  327. package/manifest/manifest.json +5090 -0
  328. package/manifest/manifest.schema.json +586 -0
  329. package/package.json +399 -5
  330. package/src/cli.ts +21 -0
  331. package/src/components/css/accordion/accordion.css +102 -0
  332. package/src/components/css/accordion/accordion.ts +50 -0
  333. package/src/components/css/alert/alert.css +53 -0
  334. package/src/components/css/alert/alert.ts +48 -0
  335. package/src/components/css/avatar/avatar.css +96 -0
  336. package/src/components/css/avatar/avatar.ts +57 -0
  337. package/src/components/css/badge/badge.css +42 -0
  338. package/src/components/css/badge/badge.ts +43 -0
  339. package/src/components/css/breadcrumbs/breadcrumbs.css +62 -0
  340. package/src/components/css/breadcrumbs/breadcrumbs.ts +30 -0
  341. package/src/components/css/button/button.css +128 -0
  342. package/src/components/css/button/button.ts +44 -0
  343. package/src/components/css/card/card.css +65 -0
  344. package/src/components/css/card/card.ts +50 -0
  345. package/src/components/css/checkbox/checkbox.css +114 -0
  346. package/src/components/css/checkbox/checkbox.ts +50 -0
  347. package/src/components/css/divider/divider.css +38 -0
  348. package/src/components/css/divider/divider.ts +42 -0
  349. package/src/components/css/error-text/error-text.css +21 -0
  350. package/src/components/css/error-text/error-text.ts +37 -0
  351. package/src/components/css/fieldset/fieldset.css +72 -0
  352. package/src/components/css/fieldset/fieldset.ts +87 -0
  353. package/src/components/css/file-upload/file-upload.css +102 -0
  354. package/src/components/css/file-upload/file-upload.ts +65 -0
  355. package/src/components/css/heading/heading.css +33 -0
  356. package/src/components/css/heading/heading.ts +42 -0
  357. package/src/components/css/help-text/help-text.css +24 -0
  358. package/src/components/css/help-text/help-text.ts +42 -0
  359. package/src/components/css/icon/icon.css +20 -0
  360. package/src/components/css/icon/icon.ts +44 -0
  361. package/src/components/css/input/input.css +145 -0
  362. package/src/components/css/input/input.ts +77 -0
  363. package/src/components/css/label/label.css +42 -0
  364. package/src/components/css/label/label.ts +51 -0
  365. package/src/components/css/link/link.css +38 -0
  366. package/src/components/css/link/link.ts +38 -0
  367. package/src/components/css/list/list.css +53 -0
  368. package/src/components/css/list/list.ts +44 -0
  369. package/src/components/css/pagination/pagination.css +95 -0
  370. package/src/components/css/pagination/pagination.ts +33 -0
  371. package/src/components/css/paragraph/paragraph.css +31 -0
  372. package/src/components/css/paragraph/paragraph.ts +53 -0
  373. package/src/components/css/progress/progress.css +161 -0
  374. package/src/components/css/progress/progress.ts +78 -0
  375. package/src/components/css/radio/radio.css +94 -0
  376. package/src/components/css/radio/radio.ts +50 -0
  377. package/src/components/css/search/search.css +50 -0
  378. package/src/components/css/search/search.ts +56 -0
  379. package/src/components/css/select/select.css +321 -0
  380. package/src/components/css/select/select.ts +63 -0
  381. package/src/components/css/shared.ts +199 -0
  382. package/src/components/css/skeleton/skeleton.css +62 -0
  383. package/src/components/css/skeleton/skeleton.ts +43 -0
  384. package/src/components/css/skip-link/skip-link.css +41 -0
  385. package/src/components/css/skip-link/skip-link.ts +22 -0
  386. package/src/components/css/spinner/spinner.css +48 -0
  387. package/src/components/css/spinner/spinner.ts +48 -0
  388. package/src/components/css/sr-only/sr-only.css +16 -0
  389. package/src/components/css/sr-only/sr-only.ts +21 -0
  390. package/src/components/css/switch/switch.css +105 -0
  391. package/src/components/css/switch/switch.ts +50 -0
  392. package/src/components/css/table/table.css +159 -0
  393. package/src/components/css/table/table.ts +107 -0
  394. package/src/components/css/tag/tag.css +58 -0
  395. package/src/components/css/tag/tag.ts +49 -0
  396. package/src/components/css/textarea/textarea.css +83 -0
  397. package/src/components/css/textarea/textarea.ts +40 -0
  398. package/src/components/css/toggle-group/toggle-group.css +92 -0
  399. package/src/components/css/toggle-group/toggle-group.ts +37 -0
  400. package/src/components/css/tooltip/tooltip.css +89 -0
  401. package/src/components/css/tooltip/tooltip.ts +37 -0
  402. package/src/components/default-texts.ts +41 -0
  403. package/src/components/events.ts +68 -0
  404. package/src/components/frame/dialog/dialog.css +306 -0
  405. package/src/components/frame/dialog/dialog.ts +166 -0
  406. package/src/components/frame/dialog/lt-dialog.ts +513 -0
  407. package/src/components/frame/error-summary/error-summary.css +47 -0
  408. package/src/components/frame/error-summary/error-summary.ts +90 -0
  409. package/src/components/frame/error-summary/lt-error-summary.ts +458 -0
  410. package/src/components/frame/field/field-core.ts +152 -0
  411. package/src/components/frame/field/field.css +17 -0
  412. package/src/components/frame/field/lt-field.ts +790 -0
  413. package/src/components/frame/popover/lt-popover.ts +586 -0
  414. package/src/components/frame/popover/popover.css +50 -0
  415. package/src/components/frame/popover/popover.ts +102 -0
  416. package/src/components/frame/session-timeout/lt-session-timeout.ts +715 -0
  417. package/src/components/frame/session-timeout/session-timeout.css +50 -0
  418. package/src/components/frame/session-timeout/session-timeout.ts +156 -0
  419. package/src/components/frame/suggestion/lt-suggestion.ts +1379 -0
  420. package/src/components/frame/suggestion/suggestion.css +132 -0
  421. package/src/components/frame/suggestion/suggestion.ts +172 -0
  422. package/src/components/frame/tabs/lt-tabs.ts +1395 -0
  423. package/src/components/frame/tabs/tabs.css +128 -0
  424. package/src/components/frame/tabs/tabs.ts +188 -0
  425. package/src/components/host-element.ts +493 -0
  426. package/src/components/standalone/connection-status/connection-status.css +41 -0
  427. package/src/components/standalone/connection-status/connection-status.ts +47 -0
  428. package/src/components/standalone/connection-status/lt-connection-status.ts +268 -0
  429. package/src/components/standalone/toast/lt-toast.ts +300 -0
  430. package/src/components/standalone/toast/toast.css +78 -0
  431. package/src/components/standalone/toast/toast.ts +70 -0
  432. package/src/diagnostics/core.ts +227 -0
  433. package/src/diagnostics/default-core.ts +81 -0
  434. package/src/diagnostics/index.ts +150 -0
  435. package/src/dom.ts +185 -0
  436. package/src/index.ts +218 -0
  437. package/src/jsx/react.ts +212 -0
  438. package/src/lt.ts +192 -0
  439. package/src/react.ts +411 -0
  440. package/src/register.ts +43 -0
  441. package/src/tailwind/tailwind.css +169 -0
  442. package/src/tokens/boundary.css +104 -0
  443. package/src/tokens/color-contract.json +231 -0
  444. package/src/tokens/color.ts +150 -0
  445. package/src/tokens/contract.ts +24 -0
  446. package/src/tokens/matrix-cells.ts +375 -0
  447. package/src/tokens/matrix.ts +32 -0
  448. package/src/tokens/theme-check.ts +51 -0
  449. package/src/tokens/theme.ts +104 -0
  450. package/src/tokens/tokens.css +569 -0
  451. package/src/tokens/tokens.ts +223 -0
  452. package/src/vocabulary/classes.ts +1356 -0
  453. package/src/vocabulary/elements.ts +492 -0
  454. package/src/vocabulary/types.ts +228 -0
  455. package/src/wasi-host.ts +155 -0
  456. package/web-types.json +800 -0
@@ -0,0 +1,715 @@
1
+ import {
2
+ addClass,
3
+ defineElement,
4
+ HostElement,
5
+ languageOf,
6
+ setAttr,
7
+ setText,
8
+ uniqueId,
9
+ warnAboutMarkup,
10
+ whenUpgraded,
11
+ } from "../../host-element.js"
12
+ import {
13
+ SESSION_LOGOUT,
14
+ SESSION_TIMEOUT_CLASS,
15
+ SESSION_TIMEOUT_COUNT_CLASS,
16
+ SESSION_TIMEOUT_DIALOG_CLASS,
17
+ SESSION_TIMEOUT_TEXT_CLASS,
18
+ } from "./session-timeout.js"
19
+
20
+ export const LT_SESSION_TIMEOUT_TAG = "lt-session-timeout" as const
21
+
22
+ /**
23
+ * The parts the warning finds, as selectors relative to the element. The
24
+ * component finds the parts with these, and the manifest describes them for
25
+ * whoever assembles the markup by hand, so the two cannot say different things.
26
+ */
27
+ export const LT_SESSION_TIMEOUT_PARTS = {
28
+ dialog: "dialog",
29
+ title: "h1, h2, h3, h4, h5, h6",
30
+ text: `.${SESSION_TIMEOUT_TEXT_CLASS}`,
31
+ count: `.${SESSION_TIMEOUT_COUNT_CLASS}`,
32
+ status: "[role='status']",
33
+ button: 'form[method="dialog" i] button',
34
+ } as const
35
+
36
+ const DEFAULT_WARN_AT = 25 * 60
37
+ const DEFAULT_EXPIRES_AT = 30 * 60
38
+
39
+ /** The seconds to announce along the way. Every second would be unbearable. */
40
+ const ANNOUNCE_AT = new Set([120, 60, 30, 10])
41
+
42
+ /**
43
+ * The activity that resets the clock.
44
+ *
45
+ * `scroll` is here even though it also fires when code scrolls. A screen
46
+ * reader in browse mode sends no key presses to the page, only scrolling, and
47
+ * so does dragging the scrollbar. Without `scroll`, someone reading a long
48
+ * page was signed out in the middle of reading. That is worse than a page
49
+ * that scrolls by itself keeping the session alive. `scroll` does not bubble,
50
+ * so everything is listened for in the capture phase, and scrolling in a
51
+ * panel counts too.
52
+ */
53
+ const ACTIVITY_EVENTS = ["pointerdown", "keydown", "scroll"] as const
54
+ const ACTIVITY_OPTIONS = { passive: true, capture: true } as const
55
+
56
+ /** Seconds between each `session-activity`. */
57
+ const DEFAULT_ACTIVITY_INTERVAL = 60
58
+
59
+ /**
60
+ * The reasons the component itself closes the dialog with.
61
+ *
62
+ * They are compared exactly, and the names are chosen so they do not collide
63
+ * with a `value` the renderer has given a button.
64
+ * `reset()` used to close with "extend", and the close was then read as the
65
+ * user pressing the button with that value.
66
+ */
67
+ const CLOSED_BY_RESET = "lt-session-reset"
68
+ const CLOSED_BY_EXPIRY = "lt-session-expired"
69
+
70
+ function clock(seconds: number): string {
71
+ const m = Math.floor(seconds / 60)
72
+ const s = seconds % 60
73
+ return `${m}:${String(s).padStart(2, "0")}`
74
+ }
75
+
76
+ /**
77
+ * The duration as it is spoken, in the page's language.
78
+ *
79
+ * `Intl` knows the words and the inflection, so "2 minutter og 30 sekunder"
80
+ * on a Norwegian page becomes "2 minutes and 30 seconds" on an English one
81
+ * without anyone translating it.
82
+ * Rounded off, the announcement said "2 minutes" while the number showed
83
+ * 1:30, so the seconds are included.
84
+ */
85
+ function spoken(seconds: number, lang: string | undefined): string {
86
+ const unit = (value: number, name: "minute" | "second") =>
87
+ new Intl.NumberFormat(lang, {
88
+ style: "unit",
89
+ unit: name,
90
+ unitDisplay: "long",
91
+ }).format(value)
92
+ const m = Math.floor(seconds / 60)
93
+ const s = seconds % 60
94
+ const parts: string[] = []
95
+ if (m > 0) parts.push(unit(m, "minute"))
96
+ if (s > 0 || m === 0) parts.push(unit(s, "second"))
97
+ return new Intl.ListFormat(lang, { type: "conjunction" }).format(parts)
98
+ }
99
+
100
+ /**
101
+ * The text in a node, with the number replaced by the duration as it is
102
+ * spoken.
103
+ *
104
+ * The text is read, not written: a copy of the paragraph with new text in it
105
+ * would have been a write, and no write in `frame/` goes around the helpers.
106
+ */
107
+ function readAloud(
108
+ node: Node,
109
+ count: Element | null,
110
+ duration: string,
111
+ ): string {
112
+ if (node === count) return duration
113
+ if (node.nodeType === Node.TEXT_NODE) return node.nodeValue ?? ""
114
+ return [...node.childNodes]
115
+ .map((child) => readAloud(child, count, duration))
116
+ .join("")
117
+ }
118
+
119
+ function isSeconds(raw: string | null): boolean {
120
+ if (raw === null) return true
121
+ const value = Number(raw)
122
+ return raw.trim() !== "" && Number.isFinite(value) && value >= 0
123
+ }
124
+
125
+ /**
126
+ * The element that has focus, whatever shadow root it is in.
127
+ *
128
+ * `document.activeElement` stops at the shadow host, and a shadow root's
129
+ * `activeElement` is null when focus is outside it. The descent goes from
130
+ * the document in through each open shadow root to the element itself.
131
+ */
132
+ function deepActiveElement(): HTMLElement | null {
133
+ let active = document.activeElement
134
+ while (active?.shadowRoot?.activeElement) {
135
+ active = active.shadowRoot.activeElement
136
+ }
137
+ return active instanceof HTMLElement ? active : null
138
+ }
139
+
140
+ /**
141
+ * Warns before a signed-in session expires.
142
+ *
143
+ * This is the thing every large organisation builds itself and gets wrong.
144
+ * Either no warning comes, and the user loses a half-filled application
145
+ * form, or the dialog steals focus in the middle of a sentence, or the
146
+ * screen reader reads the countdown every second.
147
+ *
148
+ * The renderer writes the dialog and the text, so the warning is in the
149
+ * language the app uses, with the translation tool the app already has. The
150
+ * component keeps time: it counts activity, opens the dialog with
151
+ * `showModal()`, fills in the number and announces the countdown. That is all
152
+ * it writes, along with what a template may have forgotten: the class
153
+ * `lt-session-timeout__dialog` on the dialog, `role="alertdialog"`, and the
154
+ * name from the first heading, with an id on the heading when it lacks one.
155
+ *
156
+ * The parts are recognised by what must be there anyway: a `<dialog>` inside
157
+ * the host, `.lt-session-timeout__count` for the number, `[role=status]` for
158
+ * the announcement and the buttons in a `<form method="dialog">`. A button
159
+ * with `value="logout"` logs out. Every other way of closing the dialog,
160
+ * Escape and the button with `value="extend"` included, means "I am here",
161
+ * and extends the session.
162
+ *
163
+ * ```html
164
+ * <lt-session-timeout warn-at="1500" expires-at="1800" data-ignore-morph>
165
+ * <dialog class="lt-session-timeout__dialog" role="alertdialog" aria-labelledby="session">
166
+ * <h2 class="lt-session-timeout__title" id="session">You will soon be signed out</h2>
167
+ * <p class="lt-session-timeout__text">
168
+ * We will sign you out in <span class="lt-session-timeout__count" aria-hidden="true"></span>.
169
+ * </p>
170
+ * <span class="lt-sr-only" role="status"></span>
171
+ * <form method="dialog" class="lt-session-timeout__actions">
172
+ * <button class="lt-button" value="extend">Stay signed in</button>
173
+ * <button class="lt-button" data-variant="secondary" value="logout">Sign out now</button>
174
+ * </form>
175
+ * </dialog>
176
+ * </lt-session-timeout>
177
+ * ```
178
+ *
179
+ * ```ts
180
+ * document.querySelector("lt-session-timeout")
181
+ * .addEventListener("session-extend", () => fetch("/extend", { method: "POST" }))
182
+ * ```
183
+ */
184
+ export class LtSessionTimeout extends HostElement {
185
+ static observedAttributes = [
186
+ "warn-at",
187
+ "expires-at",
188
+ "activity-interval",
189
+ ] as const
190
+
191
+ private ticker?: number
192
+ private lastActivity = Date.now()
193
+ /**
194
+ * When the component last sent `session-activity`, and whether the user has
195
+ * done anything since.
196
+ *
197
+ * The clock is reset by activity in the browser, but the server session does
198
+ * not know about it. A user typing in a long form without a single call to
199
+ * the server never got the warning, and the submission failed because the
200
+ * server session had expired. The event lets the app keep the server session
201
+ * alive. It is sent at the start of an interval, and at the end if the user
202
+ * has done something since, so the server is never more than one interval
203
+ * behind the clock here.
204
+ */
205
+ private lastActivityEvent = 0
206
+ private activitySinceEvent = false
207
+ /** The seconds left at the previous tick, so a threshold is not skipped. */
208
+ private previousLeft?: number
209
+ private previousFocus: HTMLElement | null = null
210
+ /**
211
+ * True from when the session expired, or the user chose to sign out, until
212
+ * `extend()` or `reset()` is called.
213
+ *
214
+ * Without this the cycle started again by itself, and an app that did not
215
+ * navigate away got a new dialog and a new `session-expired` every
216
+ * `expires-at` seconds, for a session that was already gone.
217
+ */
218
+ private expired = false
219
+ /** The id the component gave the heading, so the same one comes back. */
220
+ private titleId?: string
221
+ /**
222
+ * The dialog the component opened, until `close` has arrived.
223
+ *
224
+ * `close` arrives in a later task than the click that closed the dialog.
225
+ * When the clock ticked in between, `tick()` opened the dialog again and
226
+ * cleared `returnValue`, and the queued event then read `""` and extended
227
+ * the session: the user pressed "Sign out now", and the app got
228
+ * `session-extend`.
229
+ */
230
+ private shownDialog?: HTMLDialogElement
231
+ /** Watches for `open` disappearing from a dialog in the top layer. See `repairOpen()`. */
232
+ private openObserver?: MutationObserver
233
+
234
+ /** Seconds without activity before the warning appears. */
235
+ get warnAt(): number {
236
+ return this.readSeconds("warn-at", DEFAULT_WARN_AT)
237
+ }
238
+
239
+ set warnAt(value: number) {
240
+ setAttr(this, "warn-at", String(value))
241
+ }
242
+
243
+ /** Seconds between each `session-activity` while the user is active. */
244
+ get activityInterval(): number {
245
+ return this.readSeconds("activity-interval", DEFAULT_ACTIVITY_INTERVAL)
246
+ }
247
+
248
+ set activityInterval(value: number) {
249
+ setAttr(this, "activity-interval", String(value))
250
+ }
251
+
252
+ /** Seconds without activity before the session expires. */
253
+ get expiresAt(): number {
254
+ return this.readSeconds("expires-at", DEFAULT_EXPIRES_AT)
255
+ }
256
+
257
+ set expiresAt(value: number) {
258
+ setAttr(this, "expires-at", String(value))
259
+ }
260
+
261
+ connectedCallback(): void {
262
+ addClass(this, SESSION_TIMEOUT_CLASS)
263
+
264
+ for (const name of ACTIVITY_EVENTS) {
265
+ document.addEventListener(name, this.registerActivity, ACTIVITY_OPTIONS)
266
+ }
267
+ /*
268
+ * `close` does not bubble, but it passes through the host in the capture
269
+ * phase. So the close is caught even when the renderer has replaced the
270
+ * dialog, without the component having to hold on to a particular node.
271
+ */
272
+ this.addEventListener("close", this.handleClose, true)
273
+ this.openObserver = new MutationObserver(() => {
274
+ this.repairOpen()
275
+ /*
276
+ * A morph of the open warning takes the role and the name the component
277
+ * wrote, since the renderer did not send them, and the screen reader had
278
+ * a modal dialog without a name until the warning closed. They are
279
+ * written again while it shows. Such a morph also removes `open`, which
280
+ * is enough to get here; the wiring's own attributes are watched for a
281
+ * morph that leaves `open` alone.
282
+ */
283
+ const dialog = this.dialog
284
+ if (dialog?.matches(":modal")) this.wire(dialog)
285
+ })
286
+ this.openObserver.observe(this, {
287
+ subtree: true,
288
+ attributes: true,
289
+ attributeFilter: [
290
+ "open",
291
+ "role",
292
+ "class",
293
+ "id",
294
+ "aria-labelledby",
295
+ "aria-label",
296
+ ],
297
+ })
298
+ this.ticker = window.setInterval(() => this.tick(), 1000)
299
+ this.validate()
300
+
301
+ /*
302
+ * If the element is moved while the warning is showing, the browser takes
303
+ * the dialog out of the top layer and leaves `open` in place. It was then a
304
+ * box without a backdrop or a focus trap, and `tick()` never opened it
305
+ * again, since it was already "open". `removeAttribute` and not `close()`:
306
+ * a close is read as the user extending the session.
307
+ */
308
+ const dialog = this.dialog
309
+ if (dialog?.open && !dialog.matches(":modal")) {
310
+ dialog.removeAttribute("open")
311
+ dialog.showModal()
312
+ }
313
+ }
314
+
315
+ disconnectedCallback(): void {
316
+ for (const name of ACTIVITY_EVENTS) {
317
+ document.removeEventListener(
318
+ name,
319
+ this.registerActivity,
320
+ ACTIVITY_OPTIONS,
321
+ )
322
+ }
323
+ this.removeEventListener("close", this.handleClose, true)
324
+ this.openObserver?.disconnect()
325
+ this.openObserver = undefined
326
+ if (this.ticker) window.clearInterval(this.ticker)
327
+ this.ticker = undefined
328
+ }
329
+
330
+ attributeChangedCallback(): void {
331
+ if (this.isConnected) this.validate()
332
+ }
333
+
334
+ /** The dialog the component controls: the first `<dialog>` inside the host. */
335
+ private get dialog(): HTMLDialogElement | null {
336
+ return this.querySelector(LT_SESSION_TIMEOUT_PARTS.dialog)
337
+ }
338
+
339
+ private readSeconds(name: string, fallback: number): number {
340
+ const raw = this.getAttribute(name)
341
+ return isSeconds(raw) && raw !== null ? Number(raw) : fallback
342
+ }
343
+
344
+ /**
345
+ * Says so when markup and numbers do not add up, instead of staying quiet.
346
+ *
347
+ * `warn-at="1800" expires-at="1500"` gave `session-expired` without the
348
+ * dialog ever being shown, and a negative number gave a dialog on the first
349
+ * tick. The defaults are used when a number is not a number.
350
+ */
351
+ private validate(): void {
352
+ warnAboutMarkup(
353
+ this,
354
+ "has a warn-at or expires-at that is not a number greater than " +
355
+ "or equal to zero. The defaults are used instead.",
356
+ () =>
357
+ !isSeconds(this.getAttribute("warn-at")) ||
358
+ !isSeconds(this.getAttribute("expires-at")),
359
+ )
360
+ warnAboutMarkup(
361
+ this,
362
+ "has a warn-at that is not less than expires-at, so the warning " +
363
+ "never comes before the session expires.",
364
+ () => this.warnAt >= this.expiresAt,
365
+ )
366
+ warnAboutMarkup(
367
+ this,
368
+ "has an activity-interval that is not a number greater than " +
369
+ "zero. session-activity is sent at most once a second, or every " +
370
+ "60 seconds if the value is not a number.",
371
+ () => {
372
+ const raw = this.getAttribute("activity-interval")
373
+ return raw !== null && (!isSeconds(raw) || Number(raw) < 1)
374
+ },
375
+ )
376
+ // An empty element is a region that has not been filled yet, and stays
377
+ // quiet here. It is reported when the warning should have appeared. See
378
+ // `openDialog()`.
379
+ warnAboutMarkup(
380
+ this,
381
+ "found no <dialog>. The warning cannot be shown, and the " +
382
+ "session expires without warning.",
383
+ () => this.childElementCount > 0 && this.dialog === null,
384
+ )
385
+ this.validateParts()
386
+ }
387
+
388
+ /** The parts inside the dialog. Each of them makes the warning worse when missing. */
389
+ private validateParts(): void {
390
+ const dialog = this.dialog
391
+ if (!dialog) return
392
+ warnAboutMarkup(
393
+ this,
394
+ `found no .${SESSION_TIMEOUT_COUNT_CLASS} in the dialog. The user cannot see how much time is left.`,
395
+ () => !this.dialog?.querySelector(LT_SESSION_TIMEOUT_PARTS.count),
396
+ )
397
+ warnAboutMarkup(
398
+ this,
399
+ "found no [role=status] in the dialog. The countdown is not " +
400
+ "announced to screen reader users.",
401
+ () => !this.dialog?.querySelector(LT_SESSION_TIMEOUT_PARTS.status),
402
+ )
403
+ warnAboutMarkup(
404
+ this,
405
+ 'found no button in a <form method="dialog">. The user can only ' +
406
+ "extend the session with Escape, and cannot log out from the " +
407
+ "warning.",
408
+ () => !this.dialog?.querySelector(LT_SESSION_TIMEOUT_PARTS.button),
409
+ )
410
+ }
411
+
412
+ /**
413
+ * Fills in what accessibility requires and a template may have forgotten:
414
+ * the role, and the name from the first heading. Only what is missing: a
415
+ * name from `aria-label` or `aria-labelledby` was written by the renderer,
416
+ * and stays.
417
+ */
418
+ private wire(dialog: HTMLDialogElement): void {
419
+ addClass(dialog, SESSION_TIMEOUT_DIALOG_CLASS)
420
+ if (!dialog.hasAttribute("role")) setAttr(dialog, "role", "alertdialog")
421
+ if (
422
+ dialog.hasAttribute("aria-labelledby") ||
423
+ dialog.hasAttribute("aria-label")
424
+ ) {
425
+ return
426
+ }
427
+ const title = dialog.querySelector(LT_SESSION_TIMEOUT_PARTS.title)
428
+ if (!title) return
429
+ if (!title.id) {
430
+ this.titleId ??= uniqueId("lt-session-timeout-title")
431
+ setAttr(title, "id", this.titleId)
432
+ }
433
+ setAttr(dialog, "aria-labelledby", title.id)
434
+ }
435
+
436
+ /**
437
+ * Puts `open` back on a dialog that is still in the top layer.
438
+ *
439
+ * `showModal()` sets the attribute itself, and the renderer did not send
440
+ * it. A morph without `data-ignore-morph` therefore removes it, and the
441
+ * dialog stayed in the top layer, invisible, with the rest of the page
442
+ * inert. The page was stuck. The number comes back on the next tick.
443
+ *
444
+ * The condition is `:modal` and not `open`: when the user closes the
445
+ * dialog, it leaves the top layer, and then the missing attribute is real.
446
+ * The same rule as in `<lt-dialog>`.
447
+ */
448
+ private repairOpen(): void {
449
+ const dialog = this.dialog
450
+ if (!dialog?.matches(":modal") || dialog.open) return
451
+ setAttr(dialog, "open", "")
452
+ }
453
+
454
+ private registerActivity = (): void => {
455
+ if (this.dialog?.open || this.expired) return
456
+ this.lastActivity = Date.now()
457
+ this.activitySinceEvent = true
458
+ this.reportActivity()
459
+ }
460
+
461
+ /**
462
+ * Sends `session-activity` when an interval has passed and the user was
463
+ * active.
464
+ *
465
+ * Not while the warning is showing or after the session has expired: then
466
+ * the app kept the server session alive while the clock here counted down
467
+ * towards `session-expired`. The interval is at least one second, or every
468
+ * scroll sent a call.
469
+ */
470
+ private reportActivity(): void {
471
+ if (!this.activitySinceEvent || this.dialog?.open || this.expired) return
472
+ const now = Date.now()
473
+ // If the tab has been frozen past the expiry, the activity is stale. Then
474
+ // `session-activity` and `session-expired` came in the same tick, and the
475
+ // app kept alive a session the component was saying had expired.
476
+ if (now - this.lastActivity >= this.expiresAt * 1000) {
477
+ this.activitySinceEvent = false
478
+ return
479
+ }
480
+ // Scrolling comes every frame. The cheap check first, the attribute after.
481
+ const since = now - this.lastActivityEvent
482
+ if (since < 1000) return
483
+ if (since < Math.max(1, this.activityInterval) * 1000) return
484
+ this.lastActivityEvent = now
485
+ this.activitySinceEvent = false
486
+ this.emit("session-activity")
487
+ }
488
+
489
+ /**
490
+ * The user closed the dialog, or the component did.
491
+ *
492
+ * The component's own closes have their own reasons, and do nothing here.
493
+ * `value="logout"` logs out. Everything else, Escape and the button with
494
+ * `value="extend"` included, is "I am here". That also applies to a button
495
+ * with another value: otherwise the dialog closed, and the next tick opened
496
+ * it again a second later.
497
+ */
498
+ private handleClose = (event: Event): void => {
499
+ const dialog = event.target
500
+ if (!(dialog instanceof HTMLDialogElement) || dialog !== this.dialog) return
501
+ this.shownDialog = undefined
502
+
503
+ const live = dialog.querySelector(LT_SESSION_TIMEOUT_PARTS.status)
504
+ if (live) setText(live, "")
505
+ this.previousFocus?.focus()
506
+ this.previousFocus = null
507
+
508
+ const reason = dialog.returnValue
509
+ if (reason === CLOSED_BY_RESET || reason === CLOSED_BY_EXPIRY) return
510
+ if (reason === SESSION_LOGOUT) {
511
+ // As on expiry: the component stands still until `extend()` or
512
+ // `reset()`. Otherwise the next tick saw a closed dialog after the warning
513
+ // threshold and opened it again while the app was signing out.
514
+ this.expired = true
515
+ this.emit("session-logout")
516
+ return
517
+ }
518
+ this.extend()
519
+ }
520
+
521
+ private tick(): void {
522
+ if (this.expired) return
523
+ // The end of an interval: the user was active, but no event has been sent
524
+ // since the start.
525
+ this.reportActivity()
526
+
527
+ // A close is on its way. Wait for `close`, so it is read correctly. If the
528
+ // dialog has been replaced in the meantime, it never comes, and then the
529
+ // memory is released.
530
+ const current = this.dialog
531
+ /*
532
+ * The renderer replaced the dialog while the warning showed, as
533
+ * `innerHTML` does: still open when it went, so replaced, not closed.
534
+ * Decided in the tick that opens the new one, below; kept in a field, it
535
+ * outlived a `reset()` in between, and the next warning, minutes later,
536
+ * sent no `session-warn`.
537
+ */
538
+ let continuing = false
539
+ if (this.shownDialog && this.shownDialog !== current) {
540
+ continuing = this.shownDialog.open
541
+ this.shownDialog = undefined
542
+ }
543
+ if (this.shownDialog && !this.shownDialog.open) return
544
+
545
+ const elapsed = Math.floor((Date.now() - this.lastActivity) / 1000)
546
+ const left = this.expiresAt - elapsed
547
+ const dialog = this.dialog
548
+
549
+ if (left <= 0) {
550
+ // The flag first, so the close is not read as an extension.
551
+ this.expired = true
552
+ this.closeDialog(CLOSED_BY_EXPIRY)
553
+ this.emit("session-expired")
554
+ return
555
+ }
556
+
557
+ // If the dialog is already open, the countdown continues even if `warn-at`
558
+ // has been raised meanwhile. Otherwise the number froze, and the session
559
+ // expired without further warnings.
560
+ if (elapsed < this.warnAt && !dialog?.open) return
561
+
562
+ if (!dialog?.open) {
563
+ this.openDialog(left, continuing)
564
+ return
565
+ }
566
+ /*
567
+ * When a threshold is crossed, not only when it is hit exactly. A tick can
568
+ * come late, or be throttled in a background tab, and when the clock jumped
569
+ * from 61 to 59, "1 minute" was never announced.
570
+ */
571
+ const previous = this.previousLeft ?? left + 1
572
+ this.previousLeft = left
573
+ const crossed = [...ANNOUNCE_AT].some(
574
+ (limit) => previous > limit && left <= limit,
575
+ )
576
+ this.show(dialog, left, crossed)
577
+ }
578
+
579
+ /** Writes the number, and the announcement when `announce` is true. */
580
+ private show(
581
+ dialog: HTMLDialogElement,
582
+ left: number,
583
+ announce: boolean,
584
+ ): void {
585
+ const count = dialog.querySelector(LT_SESSION_TIMEOUT_PARTS.count)
586
+ if (count) setText(count, clock(left))
587
+ if (!announce) return
588
+
589
+ const live = dialog.querySelector(LT_SESSION_TIMEOUT_PARTS.status)
590
+ if (!live) return
591
+ const duration = spoken(left, languageOf(this))
592
+ /*
593
+ * The announcement is the paragraph, with the number spoken. That way it is
594
+ * in the language the renderer wrote, without a separate text for the
595
+ * screen reader. Without a paragraph holding the number, the duration is
596
+ * read on its own.
597
+ */
598
+ const text = dialog.querySelector(LT_SESSION_TIMEOUT_PARTS.text)
599
+ // If the number is outside the paragraph, the paragraph would be read without a number.
600
+ if (!text || !count || !text.contains(count)) {
601
+ setText(live, duration)
602
+ return
603
+ }
604
+ const sentence = readAloud(text, count, duration)
605
+ setText(live, sentence.replace(/\s+/g, " ").trim())
606
+ }
607
+
608
+ private openDialog(left: number, continuing = false): void {
609
+ const dialog = this.dialog
610
+ if (!dialog) {
611
+ // By now the page has long since settled, so an empty element is not a
612
+ // region waiting for content. It is a warning that will never come. If the
613
+ // element has content without a dialog, `validate()` has already said so.
614
+ warnAboutMarkup(
615
+ this,
616
+ "was about to show the warning, but found no <dialog>. Whoever " +
617
+ "renders the page must write the dialog and its text, or the " +
618
+ "user gets no warning before the session expires.",
619
+ () => this.dialog === null && this.childElementCount === 0,
620
+ )
621
+ return
622
+ }
623
+ if (dialog.open) return
624
+
625
+ this.wire(dialog)
626
+ // The dialog may have arrived after connecting, with HTML streaming in.
627
+ // Then `validate()` never saw it.
628
+ this.validateParts()
629
+ // Focus must go back to where the user was. Without this the next key
630
+ // press starts at the top of the page, in the middle of a form. Taken from
631
+ // the document down through the shadow roots, not from one root: `document`
632
+ // points to the shadow host when the field is in a shadow tree, and the
633
+ // component's root is null when the component is in a shadow root and the
634
+ // field is in the page, as `<lt-error-summary>` also warns about. The field
635
+ // can be on either side of the shadow boundary from the component, so only
636
+ // the descent finds it.
637
+ /*
638
+ * A warning that was showing when the renderer replaced the dialog, as
639
+ * `innerHTML` does, never closed: the user is still being warned. Focus
640
+ * goes back to where it was before that warning, not to `<body>`, where
641
+ * it is now, and `session-warn` has been sent already.
642
+ */
643
+ if (!continuing) this.previousFocus = deepActiveElement()
644
+ /*
645
+ * The text must be in place before the dialog opens, since `alertdialog` is
646
+ * announced as it appears. The number in the paragraph is `aria-hidden`, so
647
+ * without the announcement here the screen reader heard the paragraph
648
+ * without a number, and the first number came only at the next threshold,
649
+ * minutes later.
650
+ */
651
+ this.show(dialog, left, true)
652
+ this.previousLeft = left
653
+ // Activity before the warning is already counted in the clock here. Sent
654
+ // while the warning is showing, it kept the server session alive while the
655
+ // user was being asked to respond.
656
+ this.activitySinceEvent = false
657
+ dialog.returnValue = ""
658
+ dialog.showModal()
659
+ this.shownDialog = dialog
660
+ if (!continuing) this.emit("session-warn")
661
+ }
662
+
663
+ private closeDialog(reason: string): void {
664
+ const dialog = this.dialog
665
+ if (!dialog?.open) return
666
+ dialog.close(reason)
667
+ }
668
+
669
+ private emit(name: string): void {
670
+ this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true }))
671
+ }
672
+
673
+ /**
674
+ * Extends the session, closes the warning and sends `session-extend`.
675
+ *
676
+ * If the app listens for `session-extend` to ask the server to extend, it
677
+ * should call `reset()` when the answer comes, not this. Otherwise the
678
+ * answer sends a new `session-extend`, which sends a new call.
679
+ */
680
+ extend(): void {
681
+ this.reset()
682
+ this.emit("session-extend")
683
+ }
684
+
685
+ /**
686
+ * Resets the clock without sending `session-extend`.
687
+ *
688
+ * For an app that has already extended the session itself, such as when an
689
+ * autosave went through. `extend()` would have asked the server a second
690
+ * time.
691
+ */
692
+ reset(): void {
693
+ this.lastActivity = Date.now()
694
+ this.expired = false
695
+ this.closeDialog(CLOSED_BY_RESET)
696
+ }
697
+ }
698
+
699
+ export function defineLtSessionTimeout(
700
+ tagName: string = LT_SESSION_TIMEOUT_TAG,
701
+ ): void {
702
+ defineElement(tagName, LtSessionTimeout)
703
+ }
704
+
705
+ /** `element.extend()`, but waits for the registration first. See `whenUpgraded`. */
706
+ export async function extendSession(element: Element): Promise<void> {
707
+ const session = await whenUpgraded<LtSessionTimeout>(element)
708
+ session.extend()
709
+ }
710
+
711
+ /** `element.reset()`, but waits for the registration first. See `whenUpgraded`. */
712
+ export async function resetSession(element: Element): Promise<void> {
713
+ const session = await whenUpgraded<LtSessionTimeout>(element)
714
+ session.reset()
715
+ }