diff --git a/public/css/components/inViewTooltip.css b/public/css/components/inViewTooltip.css new file mode 100644 index 000000000..600d59302 --- /dev/null +++ b/public/css/components/inViewTooltip.css @@ -0,0 +1,81 @@ +/* the panel of js/directives/inViewTooltip.js. place() sets its place, max width and max height + inside the viewport, so neither a scrolling container nor a modal body clips it */ +.fhc-in-view-tooltip { + --fhc-in-view-tooltip-progress: var(--fhc-blue-primary, #0d6efd); + position: fixed; + z-index: 1080; + display: flex; + flex-direction: column; + width: max-content; + border: 1px solid var(--bs-border-color, #dee2e6); + border-radius: 0.375rem; + background-color: var(--bs-body-bg, #fff); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + color: var(--bs-body-color, #212529); + font-size: 0.875rem; + font-weight: normal; + line-height: 1.5; + text-align: left; +} + +/* the dark primary reaches 2:1 on the dark panel, the highlight 3.9:1 */ +html.dark .fhc-in-view-tooltip { + --fhc-in-view-tooltip-progress: var(--fhc-blue-primary-highlight, #6ea8fe); +} + +/* the body scrolls, so the progress border stays on the panel edge */ +.fhc-in-view-tooltip-body { + min-height: 0; + overflow-y: auto; + padding: 0.75rem 1rem; + white-space: pre-line; /* the text keeps its line breaks */ +} + +/* the hover timer of a sticky panel: after a pause the border fills up clockwise, then the panel sticks. + The 2 px stroke covers the 1 px border and 1 px inside it */ +.fhc-in-view-tooltip-progress { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + overflow: visible; + pointer-events: none; +} + +.fhc-in-view-tooltip-progress rect { + fill: none; + stroke: var(--fhc-in-view-tooltip-progress); + stroke-width: 2; + stroke-dasharray: var(--fhc-in-view-tooltip-perimeter); + stroke-dashoffset: var(--fhc-in-view-tooltip-perimeter); +} + +.fhc-in-view-tooltip--holding .fhc-in-view-tooltip-progress rect { + animation: fhc-in-view-tooltip-hold var(--fhc-in-view-tooltip-hold-fill) ease-in var(--fhc-in-view-tooltip-hold-delay) forwards; +} + +/* a click during the hover timer fills the rest of the border quickly */ +.fhc-in-view-tooltip--pinned .fhc-in-view-tooltip-progress rect { + stroke-dashoffset: 0; + transition: stroke-dashoffset 0.15s linear; +} + +@keyframes fhc-in-view-tooltip-hold { + to { + stroke-dashoffset: 0; + } +} + +/* the ? icon of components/InViewHelp.js takes the color of its context, e.g. white in the blue + modal header. A class like text-muted overrides it */ +.fhc-in-view-help-btn, +.fhc-in-view-help-btn:hover, +.fhc-in-view-help-btn:focus-visible, +.btn.fhc-in-view-help-btn:active { + color: inherit; +} + +.fhc-in-view-help-btn { + line-height: 1; + text-decoration: none; +} diff --git a/public/css/theme/default.css b/public/css/theme/default.css index c2a584c12..cb5530c87 100644 --- a/public/css/theme/default.css +++ b/public/css/theme/default.css @@ -333,6 +333,25 @@ html.dark .btn-danger:hover { filter: brightness(90%); } +/* the dark primevue theme draws p-button-danger (e.g. FhcAlert.confirmDelete) light pink with black text, + * use the dark red of .btn-danger */ +html.dark .p-button.p-button-danger { + color: white; + background: var(--fhc-red-60); + border-color: var(--fhc-red-60); +} + +html.dark .p-button.p-button-danger:enabled:hover, +html.dark .p-button.p-button-danger:enabled:active { + color: white; + background: var(--fhc-red-70); + border-color: var(--fhc-red-70); +} + +html.dark .p-button.p-button-danger:enabled:focus { + box-shadow: 0 0 0 0.2rem rgba(225, 110, 120, 0.5); +} + html.dark .text-danger { color: var(--fhc-danger) !important; } @@ -364,6 +383,32 @@ html.dark .btn-outline-secondary { --bs-btn-active-border-color: var(--fhc-white-90); } +/* the primevue multiselect renders the check icon also in unchecked boxes and hides it by color: + * white on white in the light theme, but #151515 on #20262e in the dark theme */ +html.dark .p-checkbox .p-checkbox-box:not(.p-highlight) .p-checkbox-icon { + color: transparent; +} + +/* FhcAlert.confirm passes bootstrap classes to the primevue buttons. The primevue theme loads after + * bootstrap, its .p-button colors replace the .btn colors: every button blue */ +.p-button.btn { + color: var(--bs-btn-color); + background: var(--bs-btn-bg); + border-color: var(--bs-btn-border-color); +} + +.p-button.btn:enabled:hover { + color: var(--bs-btn-hover-color); + background: var(--bs-btn-hover-bg); + border-color: var(--bs-btn-hover-border-color); +} + +.p-button.btn:enabled:active { + color: var(--bs-btn-active-color); + background: var(--bs-btn-active-bg); + border-color: var(--bs-btn-active-border-color); +} + html.dark .tabulator, html.dark .tabulator .tabulator-header, diff --git a/public/js/components/InViewHelp.js b/public/js/components/InViewHelp.js new file mode 100644 index 000000000..2f2586828 --- /dev/null +++ b/public/js/components/InViewHelp.js @@ -0,0 +1,34 @@ +import InViewTooltip from '../directives/inViewTooltip.js'; + +/** + * ? icon with a help text in the in-view tooltip. The panel stays inside the viewport, also at the + * screen edge, in a modal and on a phone: a plain tooltip ran out of the screen there. + * + * Help texts are long, so the panel is sticky by default: it sticks after a hover of 1.4 s or a click, + * a tap toggles it on touch. sticky: false makes it a plain hover tooltip. + * + * The view needs public/css/components/inViewTooltip.css. + * + * Usage: + */ +export default { + name: 'InViewHelp', + directives: { + tooltip: InViewTooltip + }, + props: { + text: { type: String, default: '' }, + sticky: { type: Boolean, default: true }, + buttonClass: { type: [String, Array, Object], default: '' } + }, + template: /*html*/ ` + ` +}; diff --git a/public/js/directives/inViewTooltip.js b/public/js/directives/inViewTooltip.js new file mode 100644 index 000000000..023d15755 --- /dev/null +++ b/public/js/directives/inViewTooltip.js @@ -0,0 +1,352 @@ +/** + * v-tooltip with a panel that stays in view, also at the screen edge, in a modal and on a phone. + * + * - The panel is fixed and stays inside the viewport. It opens below the anchor, above it when + * it fits only there or that side has more space. It grows away from the nearer viewport side. + * - On an anchor wider than the panel, e.g. a long table cell, it opens at the mouse, not at the anchor edge. + * - One panel at a time, appended to the body. A modal or a scrolling container cannot clip it. + * - A mouse hover or a keyboard focus opens it. Escape, a click outside or a scroll closes it. + * + * Sticky, for several sentences or a list: a short label is read before the hover pause ends, a long text is not. + * - After HOLD_DELAY_MS + HOLD_FILL_MS of hovering the panel sticks, the border fills up in the end. + * A click on the anchor makes it stick at once. Touch has no hover: a tap toggles the sticky panel. + * - A sticky panel stays until a click outside, Escape, a scroll or an opening bootstrap modal. + * + * The view needs public/css/components/inViewTooltip.css. + * + * Usage examples: + * + * 1) Directive, registered for the app or the component: + * app.directive('tooltip', InViewTooltip) or directives: { tooltip: InViewTooltip } + *