tutanota/src/common/gui/base/InfoBanner.ts
2025-10-17 15:06:50 +02:00

110 lines
3.3 KiB
TypeScript

import { AllIcons, Icon } from "./Icon.js"
import m, { Children, Component, Vnode } from "mithril"
import { theme } from "../theme.js"
import type { InfoLink, TranslationKey } from "../../misc/LanguageViewModel.js"
import { lang } from "../../misc/LanguageViewModel.js"
import type { ButtonAttrs } from "./Button.js"
import { Button, ButtonType } from "./Button.js"
import { NavButton } from "./NavButton.js"
import type { lazy } from "@tutao/tutanota-utils"
import { isNotNull } from "@tutao/tutanota-utils"
import { Icons } from "./icons/Icons.js"
import { px, size } from "../size.js"
export const enum BannerType {
Info = "info",
Warning = "warning",
}
export type BannerButtonAttrs = Omit<ButtonAttrs, "type">
export interface InfoBannerAttrs {
message: TranslationKey | lazy<Children>
icon: AllIcons
helpLink?: InfoLink | null
buttons: ReadonlyArray<BannerButtonAttrs | null>
type?: BannerType
}
/**
* A low profile banner with a message and 0 or more buttons
*/
export class InfoBanner implements Component<InfoBannerAttrs> {
view(vnode: Vnode<InfoBannerAttrs>): Children {
const { message, icon, helpLink, buttons, type } = vnode.attrs
// Adjust the top and bottom spacing because the buttons have a minimum height of 44px.
// This way the clickable area of the button overlaps with the text and the border a bit without having
// too much empty space
const buttonContainerStyle =
helpLink != null || buttons.length > 0
? {
marginTop: "-10px",
marginBottom: "-6px",
}
: undefined
return m(
".center-vertically.border-bottom.pr-4.pl-12.border-radius.mt-4",
{
style: {
border: `solid 2px ${type === BannerType.Warning ? theme.warning : theme.outline}`,
// keep the distance to the bottom of the banner the same in the case that buttons aren't present
minHeight: buttons.length > 0 ? undefined : px(37),
},
},
[
m(".mt-8.mr-8.abs", this.renderIcon(icon, type ?? null)), // absolute position makes the icon fixed to the top left corner of the banner
m(
"",
{ style: { "margin-left": px(size.icon_24 + 1) } }, // allow room for the icon
[
m(".mr-12.pt-8.pb-8", typeof message === "function" ? message() : m(".small.text-break", lang.get(message))),
m(".flex.ml-negative-8", { style: buttonContainerStyle }, [this.renderButtons(buttons), this.renderHelpLink(helpLink)]),
],
),
],
)
}
renderIcon(icon: AllIcons, type: BannerType | null): Children {
return m(Icon, {
icon,
style: {
fill: type === BannerType.Warning ? theme.warning : theme.on_surface_variant,
display: "block",
},
})
}
renderButtons(buttons: ReadonlyArray<BannerButtonAttrs | null>): Children {
if (buttons.length === 0) return null
return m(
".small.flex.row",
buttons.filter(isNotNull).map((attrs) => m(Button, { ...attrs, type: ButtonType.Secondary })),
)
}
renderHelpLink(helpLink?: InfoLink | null): Children | null {
if (helpLink == null) return null
return [
// Push the help button all the way to the right
m(".flex-grow"),
m(
".button-content",
{
style: {
marginRight: "-10px",
},
},
m(NavButton, {
icon: () => Icons.QuestionMark,
href: helpLink,
small: true,
hideLabel: true,
centred: true,
label: "help_label",
}),
),
]
}
}