@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.
- package/CHANGELOG.md +3389 -0
- package/LICENSE +21 -0
- package/README.md +129 -1
- package/agent/astro.md +400 -0
- package/agent/bundles.md +429 -0
- package/agent/datastar.md +409 -0
- package/agent/html.md +388 -0
- package/agent/react.md +440 -0
- package/agent/templates.md +472 -0
- package/core/lotek-core.wasm +0 -0
- package/core/lotek.wasm +0 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +14 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/css/accordion/accordion.d.ts +37 -0
- package/dist/components/css/accordion/accordion.d.ts.map +1 -0
- package/dist/components/css/accordion/accordion.js +31 -0
- package/dist/components/css/accordion/accordion.js.map +1 -0
- package/dist/components/css/alert/alert.d.ts +35 -0
- package/dist/components/css/alert/alert.d.ts.map +1 -0
- package/dist/components/css/alert/alert.js +29 -0
- package/dist/components/css/alert/alert.js.map +1 -0
- package/dist/components/css/avatar/avatar.d.ts +40 -0
- package/dist/components/css/avatar/avatar.d.ts.map +1 -0
- package/dist/components/css/avatar/avatar.js +30 -0
- package/dist/components/css/avatar/avatar.js.map +1 -0
- package/dist/components/css/badge/badge.d.ts +24 -0
- package/dist/components/css/badge/badge.d.ts.map +1 -0
- package/dist/components/css/badge/badge.js +24 -0
- package/dist/components/css/badge/badge.js.map +1 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.d.ts +26 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.d.ts.map +1 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.js +24 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.js.map +1 -0
- package/dist/components/css/button/button.d.ts +26 -0
- package/dist/components/css/button/button.d.ts.map +1 -0
- package/dist/components/css/button/button.js +25 -0
- package/dist/components/css/button/button.js.map +1 -0
- package/dist/components/css/card/card.d.ts +36 -0
- package/dist/components/css/card/card.d.ts.map +1 -0
- package/dist/components/css/card/card.js +28 -0
- package/dist/components/css/card/card.js.map +1 -0
- package/dist/components/css/checkbox/checkbox.d.ts +32 -0
- package/dist/components/css/checkbox/checkbox.d.ts.map +1 -0
- package/dist/components/css/checkbox/checkbox.js +27 -0
- package/dist/components/css/checkbox/checkbox.js.map +1 -0
- package/dist/components/css/divider/divider.d.ts +29 -0
- package/dist/components/css/divider/divider.d.ts.map +1 -0
- package/dist/components/css/divider/divider.js +23 -0
- package/dist/components/css/divider/divider.js.map +1 -0
- package/dist/components/css/error-text/error-text.d.ts +24 -0
- package/dist/components/css/error-text/error-text.d.ts.map +1 -0
- package/dist/components/css/error-text/error-text.js +18 -0
- package/dist/components/css/error-text/error-text.js.map +1 -0
- package/dist/components/css/fieldset/fieldset.d.ts +55 -0
- package/dist/components/css/fieldset/fieldset.d.ts.map +1 -0
- package/dist/components/css/fieldset/fieldset.js +39 -0
- package/dist/components/css/fieldset/fieldset.js.map +1 -0
- package/dist/components/css/file-upload/file-upload.d.ts +41 -0
- package/dist/components/css/file-upload/file-upload.d.ts.map +1 -0
- package/dist/components/css/file-upload/file-upload.js +32 -0
- package/dist/components/css/file-upload/file-upload.js.map +1 -0
- package/dist/components/css/heading/heading.d.ts +29 -0
- package/dist/components/css/heading/heading.d.ts.map +1 -0
- package/dist/components/css/heading/heading.js +23 -0
- package/dist/components/css/heading/heading.js.map +1 -0
- package/dist/components/css/help-text/help-text.d.ts +24 -0
- package/dist/components/css/help-text/help-text.d.ts.map +1 -0
- package/dist/components/css/help-text/help-text.js +23 -0
- package/dist/components/css/help-text/help-text.js.map +1 -0
- package/dist/components/css/icon/icon.d.ts +34 -0
- package/dist/components/css/icon/icon.d.ts.map +1 -0
- package/dist/components/css/icon/icon.js +25 -0
- package/dist/components/css/icon/icon.js.map +1 -0
- package/dist/components/css/input/input.d.ts +40 -0
- package/dist/components/css/input/input.d.ts.map +1 -0
- package/dist/components/css/input/input.js +45 -0
- package/dist/components/css/input/input.js.map +1 -0
- package/dist/components/css/label/label.d.ts +33 -0
- package/dist/components/css/label/label.d.ts.map +1 -0
- package/dist/components/css/label/label.js +23 -0
- package/dist/components/css/label/label.js.map +1 -0
- package/dist/components/css/link/link.d.ts +28 -0
- package/dist/components/css/link/link.d.ts.map +1 -0
- package/dist/components/css/link/link.js +20 -0
- package/dist/components/css/link/link.js.map +1 -0
- package/dist/components/css/list/list.d.ts +31 -0
- package/dist/components/css/list/list.d.ts.map +1 -0
- package/dist/components/css/list/list.js +25 -0
- package/dist/components/css/list/list.js.map +1 -0
- package/dist/components/css/pagination/pagination.d.ts +27 -0
- package/dist/components/css/pagination/pagination.d.ts.map +1 -0
- package/dist/components/css/pagination/pagination.js +25 -0
- package/dist/components/css/pagination/pagination.js.map +1 -0
- package/dist/components/css/paragraph/paragraph.d.ts +36 -0
- package/dist/components/css/paragraph/paragraph.d.ts.map +1 -0
- package/dist/components/css/paragraph/paragraph.js +26 -0
- package/dist/components/css/paragraph/paragraph.js.map +1 -0
- package/dist/components/css/progress/progress.d.ts +53 -0
- package/dist/components/css/progress/progress.d.ts.map +1 -0
- package/dist/components/css/progress/progress.js +38 -0
- package/dist/components/css/progress/progress.js.map +1 -0
- package/dist/components/css/radio/radio.d.ts +32 -0
- package/dist/components/css/radio/radio.d.ts.map +1 -0
- package/dist/components/css/radio/radio.js +27 -0
- package/dist/components/css/radio/radio.js.map +1 -0
- package/dist/components/css/search/search.d.ts +34 -0
- package/dist/components/css/search/search.d.ts.map +1 -0
- package/dist/components/css/search/search.js +32 -0
- package/dist/components/css/search/search.js.map +1 -0
- package/dist/components/css/select/select.d.ts +40 -0
- package/dist/components/css/select/select.d.ts.map +1 -0
- package/dist/components/css/select/select.js +31 -0
- package/dist/components/css/select/select.js.map +1 -0
- package/dist/components/css/shared.d.ts +90 -0
- package/dist/components/css/shared.d.ts.map +1 -0
- package/dist/components/css/shared.js +167 -0
- package/dist/components/css/shared.js.map +1 -0
- package/dist/components/css/skeleton/skeleton.d.ts +29 -0
- package/dist/components/css/skeleton/skeleton.d.ts.map +1 -0
- package/dist/components/css/skeleton/skeleton.js +23 -0
- package/dist/components/css/skeleton/skeleton.js.map +1 -0
- package/dist/components/css/skip-link/skip-link.d.ts +18 -0
- package/dist/components/css/skip-link/skip-link.d.ts.map +1 -0
- package/dist/components/css/skip-link/skip-link.js +16 -0
- package/dist/components/css/skip-link/skip-link.js.map +1 -0
- package/dist/components/css/spinner/spinner.d.ts +33 -0
- package/dist/components/css/spinner/spinner.d.ts.map +1 -0
- package/dist/components/css/spinner/spinner.js +25 -0
- package/dist/components/css/spinner/spinner.js.map +1 -0
- package/dist/components/css/sr-only/sr-only.d.ts +17 -0
- package/dist/components/css/sr-only/sr-only.d.ts.map +1 -0
- package/dist/components/css/sr-only/sr-only.js +15 -0
- package/dist/components/css/sr-only/sr-only.js.map +1 -0
- package/dist/components/css/switch/switch.d.ts +37 -0
- package/dist/components/css/switch/switch.d.ts.map +1 -0
- package/dist/components/css/switch/switch.js +29 -0
- package/dist/components/css/switch/switch.js.map +1 -0
- package/dist/components/css/table/table.d.ts +76 -0
- package/dist/components/css/table/table.d.ts.map +1 -0
- package/dist/components/css/table/table.js +59 -0
- package/dist/components/css/table/table.js.map +1 -0
- package/dist/components/css/tag/tag.d.ts +35 -0
- package/dist/components/css/tag/tag.d.ts.map +1 -0
- package/dist/components/css/tag/tag.js +27 -0
- package/dist/components/css/tag/tag.js.map +1 -0
- package/dist/components/css/textarea/textarea.d.ts +23 -0
- package/dist/components/css/textarea/textarea.d.ts.map +1 -0
- package/dist/components/css/textarea/textarea.js +18 -0
- package/dist/components/css/textarea/textarea.js.map +1 -0
- package/dist/components/css/toggle-group/toggle-group.d.ts +31 -0
- package/dist/components/css/toggle-group/toggle-group.d.ts.map +1 -0
- package/dist/components/css/toggle-group/toggle-group.js +29 -0
- package/dist/components/css/toggle-group/toggle-group.js.map +1 -0
- package/dist/components/css/tooltip/tooltip.d.ts +31 -0
- package/dist/components/css/tooltip/tooltip.d.ts.map +1 -0
- package/dist/components/css/tooltip/tooltip.js +29 -0
- package/dist/components/css/tooltip/tooltip.js.map +1 -0
- package/dist/components/default-texts.d.ts +41 -0
- package/dist/components/default-texts.d.ts.map +1 -0
- package/dist/components/default-texts.js +41 -0
- package/dist/components/default-texts.js.map +1 -0
- package/dist/components/events.d.ts +77 -0
- package/dist/components/events.d.ts.map +1 -0
- package/dist/components/events.js +3 -0
- package/dist/components/events.js.map +1 -0
- package/dist/components/frame/dialog/dialog.d.ts +122 -0
- package/dist/components/frame/dialog/dialog.d.ts.map +1 -0
- package/dist/components/frame/dialog/dialog.js +112 -0
- package/dist/components/frame/dialog/dialog.js.map +1 -0
- package/dist/components/frame/dialog/lt-dialog.d.ts +104 -0
- package/dist/components/frame/dialog/lt-dialog.d.ts.map +1 -0
- package/dist/components/frame/dialog/lt-dialog.js +434 -0
- package/dist/components/frame/dialog/lt-dialog.js.map +1 -0
- package/dist/components/frame/error-summary/error-summary.d.ts +53 -0
- package/dist/components/frame/error-summary/error-summary.d.ts.map +1 -0
- package/dist/components/frame/error-summary/error-summary.js +54 -0
- package/dist/components/frame/error-summary/error-summary.js.map +1 -0
- package/dist/components/frame/error-summary/lt-error-summary.d.ts +93 -0
- package/dist/components/frame/error-summary/lt-error-summary.d.ts.map +1 -0
- package/dist/components/frame/error-summary/lt-error-summary.js +408 -0
- package/dist/components/frame/error-summary/lt-error-summary.js.map +1 -0
- package/dist/components/frame/field/field-core.d.ts +92 -0
- package/dist/components/frame/field/field-core.d.ts.map +1 -0
- package/dist/components/frame/field/field-core.js +63 -0
- package/dist/components/frame/field/field-core.js.map +1 -0
- package/dist/components/frame/field/lt-field.d.ts +173 -0
- package/dist/components/frame/field/lt-field.d.ts.map +1 -0
- package/dist/components/frame/field/lt-field.js +600 -0
- package/dist/components/frame/field/lt-field.js.map +1 -0
- package/dist/components/frame/popover/lt-popover.d.ts +136 -0
- package/dist/components/frame/popover/lt-popover.d.ts.map +1 -0
- package/dist/components/frame/popover/lt-popover.js +520 -0
- package/dist/components/frame/popover/lt-popover.js.map +1 -0
- package/dist/components/frame/popover/popover.d.ts +49 -0
- package/dist/components/frame/popover/popover.d.ts.map +1 -0
- package/dist/components/frame/popover/popover.js +64 -0
- package/dist/components/frame/popover/popover.js.map +1 -0
- package/dist/components/frame/session-timeout/lt-session-timeout.d.ts +198 -0
- package/dist/components/frame/session-timeout/lt-session-timeout.d.ts.map +1 -0
- package/dist/components/frame/session-timeout/lt-session-timeout.js +610 -0
- package/dist/components/frame/session-timeout/lt-session-timeout.js.map +1 -0
- package/dist/components/frame/session-timeout/session-timeout.d.ts +113 -0
- package/dist/components/frame/session-timeout/session-timeout.d.ts.map +1 -0
- package/dist/components/frame/session-timeout/session-timeout.js +101 -0
- package/dist/components/frame/session-timeout/session-timeout.js.map +1 -0
- package/dist/components/frame/suggestion/lt-suggestion.d.ts +367 -0
- package/dist/components/frame/suggestion/lt-suggestion.d.ts.map +1 -0
- package/dist/components/frame/suggestion/lt-suggestion.js +1171 -0
- package/dist/components/frame/suggestion/lt-suggestion.js.map +1 -0
- package/dist/components/frame/suggestion/suggestion.d.ts +143 -0
- package/dist/components/frame/suggestion/suggestion.d.ts.map +1 -0
- package/dist/components/frame/suggestion/suggestion.js +127 -0
- package/dist/components/frame/suggestion/suggestion.js.map +1 -0
- package/dist/components/frame/tabs/lt-tabs.d.ts +426 -0
- package/dist/components/frame/tabs/lt-tabs.d.ts.map +1 -0
- package/dist/components/frame/tabs/lt-tabs.js +1265 -0
- package/dist/components/frame/tabs/lt-tabs.js.map +1 -0
- package/dist/components/frame/tabs/tabs.d.ts +112 -0
- package/dist/components/frame/tabs/tabs.d.ts.map +1 -0
- package/dist/components/frame/tabs/tabs.js +107 -0
- package/dist/components/frame/tabs/tabs.js.map +1 -0
- package/dist/components/host-element.d.ts +184 -0
- package/dist/components/host-element.d.ts.map +1 -0
- package/dist/components/host-element.js +440 -0
- package/dist/components/host-element.js.map +1 -0
- package/dist/components/standalone/connection-status/connection-status.d.ts +32 -0
- package/dist/components/standalone/connection-status/connection-status.d.ts.map +1 -0
- package/dist/components/standalone/connection-status/connection-status.js +26 -0
- package/dist/components/standalone/connection-status/connection-status.js.map +1 -0
- package/dist/components/standalone/connection-status/lt-connection-status.d.ts +86 -0
- package/dist/components/standalone/connection-status/lt-connection-status.d.ts.map +1 -0
- package/dist/components/standalone/connection-status/lt-connection-status.js +240 -0
- package/dist/components/standalone/connection-status/lt-connection-status.js.map +1 -0
- package/dist/components/standalone/toast/lt-toast.d.ts +109 -0
- package/dist/components/standalone/toast/lt-toast.d.ts.map +1 -0
- package/dist/components/standalone/toast/lt-toast.js +256 -0
- package/dist/components/standalone/toast/lt-toast.js.map +1 -0
- package/dist/components/standalone/toast/toast.d.ts +52 -0
- package/dist/components/standalone/toast/toast.d.ts.map +1 -0
- package/dist/components/standalone/toast/toast.js +42 -0
- package/dist/components/standalone/toast/toast.js.map +1 -0
- package/dist/diagnostics/core.d.ts +171 -0
- package/dist/diagnostics/core.d.ts.map +1 -0
- package/dist/diagnostics/core.js +84 -0
- package/dist/diagnostics/core.js.map +1 -0
- package/dist/diagnostics/default-core.d.ts +10 -0
- package/dist/diagnostics/default-core.d.ts.map +1 -0
- package/dist/diagnostics/default-core.js +72 -0
- package/dist/diagnostics/default-core.js.map +1 -0
- package/dist/diagnostics/index.d.ts +84 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +106 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/dom.d.ts +55 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +162 -0
- package/dist/dom.js.map +1 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/jsx/react.d.ts +180 -0
- package/dist/jsx/react.d.ts.map +1 -0
- package/dist/jsx/react.js +2 -0
- package/dist/jsx/react.js.map +1 -0
- package/dist/lotek.css +3617 -0
- package/dist/lt.d.ts +325 -0
- package/dist/lt.d.ts.map +1 -0
- package/dist/lt.js +183 -0
- package/dist/lt.js.map +1 -0
- package/dist/react.d.ts +494 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +296 -0
- package/dist/react.js.map +1 -0
- package/dist/register.d.ts +17 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +35 -0
- package/dist/register.js.map +1 -0
- package/dist/tokens/color.d.ts +43 -0
- package/dist/tokens/color.d.ts.map +1 -0
- package/dist/tokens/color.js +123 -0
- package/dist/tokens/color.js.map +1 -0
- package/dist/tokens/contract.d.ts +18 -0
- package/dist/tokens/contract.d.ts.map +1 -0
- package/dist/tokens/contract.js +18 -0
- package/dist/tokens/contract.js.map +1 -0
- package/dist/tokens/matrix-cells.d.ts +30 -0
- package/dist/tokens/matrix-cells.d.ts.map +1 -0
- package/dist/tokens/matrix-cells.js +273 -0
- package/dist/tokens/matrix-cells.js.map +1 -0
- package/dist/tokens/matrix.d.ts +18 -0
- package/dist/tokens/matrix.d.ts.map +1 -0
- package/dist/tokens/matrix.js +21 -0
- package/dist/tokens/matrix.js.map +1 -0
- package/dist/tokens/theme-check.d.ts +42 -0
- package/dist/tokens/theme-check.d.ts.map +1 -0
- package/dist/tokens/theme-check.js +10 -0
- package/dist/tokens/theme-check.js.map +1 -0
- package/dist/tokens/theme.d.ts +94 -0
- package/dist/tokens/theme.d.ts.map +1 -0
- package/dist/tokens/theme.js +14 -0
- package/dist/tokens/theme.js.map +1 -0
- package/dist/tokens/tokens.d.ts +122 -0
- package/dist/tokens/tokens.d.ts.map +1 -0
- package/dist/tokens/tokens.js +158 -0
- package/dist/tokens/tokens.js.map +1 -0
- package/dist/vocabulary/classes.d.ts +3 -0
- package/dist/vocabulary/classes.d.ts.map +1 -0
- package/dist/vocabulary/classes.js +1354 -0
- package/dist/vocabulary/classes.js.map +1 -0
- package/dist/vocabulary/elements.d.ts +3 -0
- package/dist/vocabulary/elements.d.ts.map +1 -0
- package/dist/vocabulary/elements.js +490 -0
- package/dist/vocabulary/elements.js.map +1 -0
- package/dist/vocabulary/types.d.ts +229 -0
- package/dist/vocabulary/types.d.ts.map +1 -0
- package/dist/vocabulary/types.js +11 -0
- package/dist/vocabulary/types.js.map +1 -0
- package/dist/wasi-host.d.ts +24 -0
- package/dist/wasi-host.d.ts.map +1 -0
- package/dist/wasi-host.js +140 -0
- package/dist/wasi-host.js.map +1 -0
- package/live-templates.xml +63 -0
- package/manifest/files.json +373 -0
- package/manifest/manifest.json +5090 -0
- package/manifest/manifest.schema.json +586 -0
- package/package.json +399 -5
- package/src/cli.ts +21 -0
- package/src/components/css/accordion/accordion.css +102 -0
- package/src/components/css/accordion/accordion.ts +50 -0
- package/src/components/css/alert/alert.css +53 -0
- package/src/components/css/alert/alert.ts +48 -0
- package/src/components/css/avatar/avatar.css +96 -0
- package/src/components/css/avatar/avatar.ts +57 -0
- package/src/components/css/badge/badge.css +42 -0
- package/src/components/css/badge/badge.ts +43 -0
- package/src/components/css/breadcrumbs/breadcrumbs.css +62 -0
- package/src/components/css/breadcrumbs/breadcrumbs.ts +30 -0
- package/src/components/css/button/button.css +128 -0
- package/src/components/css/button/button.ts +44 -0
- package/src/components/css/card/card.css +65 -0
- package/src/components/css/card/card.ts +50 -0
- package/src/components/css/checkbox/checkbox.css +114 -0
- package/src/components/css/checkbox/checkbox.ts +50 -0
- package/src/components/css/divider/divider.css +38 -0
- package/src/components/css/divider/divider.ts +42 -0
- package/src/components/css/error-text/error-text.css +21 -0
- package/src/components/css/error-text/error-text.ts +37 -0
- package/src/components/css/fieldset/fieldset.css +72 -0
- package/src/components/css/fieldset/fieldset.ts +87 -0
- package/src/components/css/file-upload/file-upload.css +102 -0
- package/src/components/css/file-upload/file-upload.ts +65 -0
- package/src/components/css/heading/heading.css +33 -0
- package/src/components/css/heading/heading.ts +42 -0
- package/src/components/css/help-text/help-text.css +24 -0
- package/src/components/css/help-text/help-text.ts +42 -0
- package/src/components/css/icon/icon.css +20 -0
- package/src/components/css/icon/icon.ts +44 -0
- package/src/components/css/input/input.css +145 -0
- package/src/components/css/input/input.ts +77 -0
- package/src/components/css/label/label.css +42 -0
- package/src/components/css/label/label.ts +51 -0
- package/src/components/css/link/link.css +38 -0
- package/src/components/css/link/link.ts +38 -0
- package/src/components/css/list/list.css +53 -0
- package/src/components/css/list/list.ts +44 -0
- package/src/components/css/pagination/pagination.css +95 -0
- package/src/components/css/pagination/pagination.ts +33 -0
- package/src/components/css/paragraph/paragraph.css +31 -0
- package/src/components/css/paragraph/paragraph.ts +53 -0
- package/src/components/css/progress/progress.css +161 -0
- package/src/components/css/progress/progress.ts +78 -0
- package/src/components/css/radio/radio.css +94 -0
- package/src/components/css/radio/radio.ts +50 -0
- package/src/components/css/search/search.css +50 -0
- package/src/components/css/search/search.ts +56 -0
- package/src/components/css/select/select.css +321 -0
- package/src/components/css/select/select.ts +63 -0
- package/src/components/css/shared.ts +199 -0
- package/src/components/css/skeleton/skeleton.css +62 -0
- package/src/components/css/skeleton/skeleton.ts +43 -0
- package/src/components/css/skip-link/skip-link.css +41 -0
- package/src/components/css/skip-link/skip-link.ts +22 -0
- package/src/components/css/spinner/spinner.css +48 -0
- package/src/components/css/spinner/spinner.ts +48 -0
- package/src/components/css/sr-only/sr-only.css +16 -0
- package/src/components/css/sr-only/sr-only.ts +21 -0
- package/src/components/css/switch/switch.css +105 -0
- package/src/components/css/switch/switch.ts +50 -0
- package/src/components/css/table/table.css +159 -0
- package/src/components/css/table/table.ts +107 -0
- package/src/components/css/tag/tag.css +58 -0
- package/src/components/css/tag/tag.ts +49 -0
- package/src/components/css/textarea/textarea.css +83 -0
- package/src/components/css/textarea/textarea.ts +40 -0
- package/src/components/css/toggle-group/toggle-group.css +92 -0
- package/src/components/css/toggle-group/toggle-group.ts +37 -0
- package/src/components/css/tooltip/tooltip.css +89 -0
- package/src/components/css/tooltip/tooltip.ts +37 -0
- package/src/components/default-texts.ts +41 -0
- package/src/components/events.ts +68 -0
- package/src/components/frame/dialog/dialog.css +306 -0
- package/src/components/frame/dialog/dialog.ts +166 -0
- package/src/components/frame/dialog/lt-dialog.ts +513 -0
- package/src/components/frame/error-summary/error-summary.css +47 -0
- package/src/components/frame/error-summary/error-summary.ts +90 -0
- package/src/components/frame/error-summary/lt-error-summary.ts +458 -0
- package/src/components/frame/field/field-core.ts +152 -0
- package/src/components/frame/field/field.css +17 -0
- package/src/components/frame/field/lt-field.ts +790 -0
- package/src/components/frame/popover/lt-popover.ts +586 -0
- package/src/components/frame/popover/popover.css +50 -0
- package/src/components/frame/popover/popover.ts +102 -0
- package/src/components/frame/session-timeout/lt-session-timeout.ts +715 -0
- package/src/components/frame/session-timeout/session-timeout.css +50 -0
- package/src/components/frame/session-timeout/session-timeout.ts +156 -0
- package/src/components/frame/suggestion/lt-suggestion.ts +1379 -0
- package/src/components/frame/suggestion/suggestion.css +132 -0
- package/src/components/frame/suggestion/suggestion.ts +172 -0
- package/src/components/frame/tabs/lt-tabs.ts +1395 -0
- package/src/components/frame/tabs/tabs.css +128 -0
- package/src/components/frame/tabs/tabs.ts +188 -0
- package/src/components/host-element.ts +493 -0
- package/src/components/standalone/connection-status/connection-status.css +41 -0
- package/src/components/standalone/connection-status/connection-status.ts +47 -0
- package/src/components/standalone/connection-status/lt-connection-status.ts +268 -0
- package/src/components/standalone/toast/lt-toast.ts +300 -0
- package/src/components/standalone/toast/toast.css +78 -0
- package/src/components/standalone/toast/toast.ts +70 -0
- package/src/diagnostics/core.ts +227 -0
- package/src/diagnostics/default-core.ts +81 -0
- package/src/diagnostics/index.ts +150 -0
- package/src/dom.ts +185 -0
- package/src/index.ts +218 -0
- package/src/jsx/react.ts +212 -0
- package/src/lt.ts +192 -0
- package/src/react.ts +411 -0
- package/src/register.ts +43 -0
- package/src/tailwind/tailwind.css +169 -0
- package/src/tokens/boundary.css +104 -0
- package/src/tokens/color-contract.json +231 -0
- package/src/tokens/color.ts +150 -0
- package/src/tokens/contract.ts +24 -0
- package/src/tokens/matrix-cells.ts +375 -0
- package/src/tokens/matrix.ts +32 -0
- package/src/tokens/theme-check.ts +51 -0
- package/src/tokens/theme.ts +104 -0
- package/src/tokens/tokens.css +569 -0
- package/src/tokens/tokens.ts +223 -0
- package/src/vocabulary/classes.ts +1356 -0
- package/src/vocabulary/elements.ts +492 -0
- package/src/vocabulary/types.ts +228 -0
- package/src/wasi-host.ts +155 -0
- 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
|
+
}
|