1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-25871] updated phishing warning UI (#16748)

* refactor phishing-warning.component

* add hideBackground input to anon-layout component

* add icon tile component to CL

* add storybook story; fix binding bug in template

* export icon-tile from CL

* update design of phishing warning page

* revert icon button to use string type; add comment to icon scss

* update callout to allow no icon/title on all variants

* update phishing warning styles

* fix defects

* crowdin messages cannot be changed, they must be replaced

* add global css override

* add phishing help link

* update icon used in tile

* tweak styles
This commit is contained in:
Will Martin
2025-10-15 10:32:02 -04:00
committed by GitHub
parent fdee84214a
commit 0713f90a06
23 changed files with 623 additions and 68 deletions

View File

@@ -100,6 +100,7 @@ $icomoon-font-path: "~@bitwarden/angular/src/scss/bwicons/fonts/" !default;
}
// For new icons - add their glyph name and value to the map below
// Also add to `libs/components/src/shared/icon.ts`
$icons: (
"angle-down": "\e900",
"angle-left": "\e901",

View File

@@ -6,6 +6,7 @@
[maxWidth]="maxWidth"
[hideCardWrapper]="hideCardWrapper"
[hideIcon]="hideIcon"
[hideBackgroundIllustration]="hideBackgroundIllustration"
>
<router-outlet></router-outlet>
<router-outlet slot="secondary" name="secondary"></router-outlet>

View File

@@ -44,6 +44,10 @@ export interface AnonLayoutWrapperData {
* Hide the card that wraps the default content. Defaults to false.
*/
hideCardWrapper?: boolean;
/**
* Hides the background illustration. Defaults to false.
*/
hideBackgroundIllustration?: boolean;
}
@Component({
@@ -60,6 +64,7 @@ export class AnonLayoutWrapperComponent implements OnInit {
protected maxWidth?: AnonLayoutMaxWidth | null;
protected hideCardWrapper?: boolean | null;
protected hideIcon?: boolean | null;
protected hideBackgroundIllustration?: boolean | null;
constructor(
private router: Router,
@@ -117,6 +122,7 @@ export class AnonLayoutWrapperComponent implements OnInit {
this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]);
this.maxWidth = firstChildRouteData["maxWidth"];
this.hideCardWrapper = Boolean(firstChildRouteData["hideCardWrapper"]);
this.hideBackgroundIllustration = Boolean(firstChildRouteData["hideBackgroundIllustration"]);
}
private listenForServiceDataChanges() {
@@ -157,6 +163,10 @@ export class AnonLayoutWrapperComponent implements OnInit {
this.hideCardWrapper = data.hideCardWrapper;
}
if (data.hideBackgroundIllustration !== undefined) {
this.hideBackgroundIllustration = data.hideBackgroundIllustration;
}
if (data.hideIcon !== undefined) {
this.hideIcon = data.hideIcon;
}
@@ -188,5 +198,6 @@ export class AnonLayoutWrapperComponent implements OnInit {
this.maxWidth = null;
this.hideCardWrapper = null;
this.hideIcon = null;
this.hideBackgroundIllustration = null;
}
}

View File

@@ -68,16 +68,18 @@
</ng-container>
</footer>
<div
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-start-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
>
<bit-icon [icon]="leftIllustration"></bit-icon>
</div>
<div
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-end-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
>
<bit-icon [icon]="rightIllustration"></bit-icon>
</div>
@if (!hideBackgroundIllustration()) {
<div
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-start-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
>
<bit-icon [icon]="leftIllustration"></bit-icon>
</div>
<div
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-end-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
>
<bit-icon [icon]="rightIllustration"></bit-icon>
</div>
}
</main>
<ng-template #defaultContent>

View File

@@ -51,6 +51,7 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
readonly hideFooter = input<boolean>(false);
readonly hideIcon = input<boolean>(false);
readonly hideCardWrapper = input<boolean>(false);
readonly hideBackgroundIllustration = input<boolean>(false);
/**
* Max width of the anon layout title, subtitle, and content areas.

View File

@@ -79,6 +79,7 @@ export default {
[hideIcon]="hideIcon"
[hideLogo]="hideLogo"
[hideFooter]="hideFooter"
[hideBackgroundIllustration]="hideBackgroundIllustration"
>
<ng-container [ngSwitch]="contentLength">
<div *ngSwitchCase="'thin'" class="tw-text-center"> <div class="tw-font-bold">Thin Content</div></div>
@@ -125,6 +126,7 @@ export default {
hideIcon: { control: "boolean" },
hideLogo: { control: "boolean" },
hideFooter: { control: "boolean" },
hideBackgroundIllustration: { control: "boolean" },
contentLength: {
control: "radio",
@@ -145,6 +147,7 @@ export default {
hideIcon: false,
hideLogo: false,
hideFooter: false,
hideBackgroundIllustration: false,
contentLength: "normal",
showSecondary: false,
},
@@ -221,6 +224,10 @@ export const NoFooter: Story = {
args: { hideFooter: true },
};
export const NoBackgroundIllustration: Story = {
args: { hideBackgroundIllustration: true },
};
export const ReadonlyHostname: Story = {
args: { showReadonlyHostname: true },
};
@@ -234,5 +241,6 @@ export const MinimalState: Story = {
hideIcon: true,
hideLogo: true,
hideFooter: true,
hideBackgroundIllustration: true,
},
};

View File

@@ -36,11 +36,17 @@ let nextId = 0;
export class CalloutComponent {
readonly type = input<CalloutTypes>("info");
readonly icon = input<string>();
readonly title = input<string>();
readonly title = input<string | null>();
readonly useAlertRole = input(false);
readonly iconComputed = computed(() => this.icon() ?? defaultIcon[this.type()]);
readonly iconComputed = computed(() =>
this.icon() === undefined ? defaultIcon[this.type()] : this.icon(),
);
readonly titleComputed = computed(() => {
const title = this.title();
if (title === null) {
return undefined;
}
const type = this.type();
if (title == null && defaultI18n[type] != null) {
return this.i18nService.t(defaultI18n[type]);

View File

@@ -0,0 +1,7 @@
<div
[ngClass]="containerClasses()"
[attr.aria-label]="ariaLabel()"
[attr.role]="ariaLabel() ? 'img' : null"
>
<i [ngClass]="iconClasses()" aria-hidden="true"></i>
</div>

View File

@@ -0,0 +1,111 @@
import { NgClass } from "@angular/common";
import { Component, computed, input } from "@angular/core";
import { BitwardenIcon } from "../shared/icon";
export type IconTileVariant = "primary" | "success" | "warning" | "danger" | "muted";
export type IconTileSize = "small" | "default" | "large";
export type IconTileShape = "square" | "circle";
const variantStyles: Record<IconTileVariant, string[]> = {
primary: ["tw-bg-primary-100", "tw-text-primary-700"],
success: ["tw-bg-success-100", "tw-text-success-700"],
warning: ["tw-bg-warning-100", "tw-text-warning-700"],
danger: ["tw-bg-danger-100", "tw-text-danger-700"],
muted: ["tw-bg-secondary-100", "tw-text-secondary-700"],
};
const sizeStyles: Record<IconTileSize, { container: string[]; icon: string[] }> = {
small: {
container: ["tw-w-6", "tw-h-6"],
icon: ["tw-text-sm"],
},
default: {
container: ["tw-w-8", "tw-h-8"],
icon: ["tw-text-base"],
},
large: {
container: ["tw-w-10", "tw-h-10"],
icon: ["tw-text-lg"],
},
};
const shapeStyles: Record<IconTileShape, Record<IconTileSize, string[]>> = {
square: {
small: ["tw-rounded"],
default: ["tw-rounded-md"],
large: ["tw-rounded-lg"],
},
circle: {
small: ["tw-rounded-full"],
default: ["tw-rounded-full"],
large: ["tw-rounded-full"],
},
};
/**
* Icon tiles are static containers that display an icon with a colored background.
* They are similar to icon buttons but are not interactive and are used for visual
* indicators, status representations, or decorative elements.
*
* Use icon tiles to:
* - Display status or category indicators
* - Represent different types of content
* - Create visual hierarchy in lists or cards
* - Show app or service icons in a consistent format
*/
@Component({
selector: "bit-icon-tile",
templateUrl: "icon-tile.component.html",
imports: [NgClass],
})
export class IconTileComponent {
/**
* The BWI icon name
*/
readonly icon = input.required<BitwardenIcon>();
/**
* The visual theme of the icon tile
*/
readonly variant = input<IconTileVariant>("primary");
/**
* The size of the icon tile
*/
readonly size = input<IconTileSize>("default");
/**
* The shape of the icon tile
*/
readonly shape = input<IconTileShape>("square");
/**
* Optional aria-label for accessibility when the icon has semantic meaning
*/
readonly ariaLabel = input<string>();
protected readonly containerClasses = computed(() => {
const variant = this.variant();
const size = this.size();
const shape = this.shape();
return [
"tw-inline-flex",
"tw-items-center",
"tw-justify-center",
"tw-flex-shrink-0",
...variantStyles[variant],
...sizeStyles[size].container,
...shapeStyles[shape][size],
];
});
protected readonly iconClasses = computed(() => {
const size = this.size();
return ["bwi", this.icon(), ...sizeStyles[size].icon];
});
}

View File

@@ -0,0 +1,114 @@
import { Meta, StoryObj } from "@storybook/angular";
import { BITWARDEN_ICONS } from "../shared/icon";
import { IconTileComponent } from "./icon-tile.component";
export default {
title: "Component Library/Icon Tile",
component: IconTileComponent,
args: {
icon: "bwi-star",
variant: "primary",
size: "default",
shape: "square",
},
argTypes: {
variant: {
options: ["primary", "success", "warning", "danger", "muted"],
control: { type: "select" },
},
size: {
options: ["small", "default", "large"],
control: { type: "select" },
},
shape: {
options: ["square", "circle"],
control: { type: "select" },
},
icon: {
options: BITWARDEN_ICONS,
control: { type: "select" },
},
ariaLabel: {
control: { type: "text" },
},
},
parameters: {
design: {
type: "figma",
url: "https://atlassian.design/components/icon/icon-tile/examples",
},
},
} as Meta<IconTileComponent>;
type Story = StoryObj<IconTileComponent>;
export const Default: Story = {};
export const AllVariants: Story = {
render: () => ({
template: `
<div class="tw-flex tw-gap-4 tw-items-center tw-flex-wrap">
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2">
<bit-icon-tile icon="bwi-collection" variant="primary"></bit-icon-tile>
<span class="tw-text-sm tw-text-muted">Primary</span>
</div>
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2">
<bit-icon-tile icon="bwi-check-circle" variant="success"></bit-icon-tile>
<span class="tw-text-sm tw-text-muted">Success</span>
</div>
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2">
<bit-icon-tile icon="bwi-exclamation-triangle" variant="warning"></bit-icon-tile>
<span class="tw-text-sm tw-text-muted">Warning</span>
</div>
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2">
<bit-icon-tile icon="bwi-error" variant="danger"></bit-icon-tile>
<span class="tw-text-sm tw-text-muted">Danger</span>
</div>
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2">
<bit-icon-tile icon="bwi-question-circle" variant="muted"></bit-icon-tile>
<span class="tw-text-sm tw-text-muted">Muted</span>
</div>
</div>
`,
}),
};
export const AllSizes: Story = {
render: () => ({
template: `
<div class="tw-flex tw-gap-4 tw-items-center">
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2">
<bit-icon-tile icon="bwi-star" variant="primary" size="small"></bit-icon-tile>
<span class="tw-text-sm tw-text-muted">Small</span>
</div>
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2">
<bit-icon-tile icon="bwi-star" variant="primary" size="default"></bit-icon-tile>
<span class="tw-text-sm tw-text-muted">Default</span>
</div>
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2">
<bit-icon-tile icon="bwi-star" variant="primary" size="large"></bit-icon-tile>
<span class="tw-text-sm tw-text-muted">Large</span>
</div>
</div>
`,
}),
};
export const AllShapes: Story = {
render: () => ({
template: `
<div class="tw-flex tw-gap-4 tw-items-center">
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2">
<bit-icon-tile icon="bwi-user" variant="primary" shape="square"></bit-icon-tile>
<span class="tw-text-sm tw-text-muted">Square</span>
</div>
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2">
<bit-icon-tile icon="bwi-user" variant="primary" shape="circle"></bit-icon-tile>
<span class="tw-text-sm tw-text-muted">Circle</span>
</div>
</div>
`,
}),
};

View File

@@ -0,0 +1 @@
export * from "./icon-tile.component";

View File

@@ -21,6 +21,7 @@ export * from "./drawer";
export * from "./form-field";
export * from "./icon-button";
export * from "./icon";
export * from "./icon-tile";
export * from "./input";
export * from "./item";
export * from "./layout";

View File

@@ -0,0 +1,110 @@
/**
* Array of available Bitwarden Web Icons (bwi) font names.
* These correspond to the actual icon names defined in the bwi-font.
* This array serves as the single source of truth for all available icons.
*/
export const BITWARDEN_ICONS = [
"bwi-angle-down",
"bwi-angle-left",
"bwi-angle-right",
"bwi-angle-up",
"bwi-archive",
"bwi-bell",
"bwi-billing",
"bwi-bitcoin",
"bwi-browser",
"bwi-browser-alt",
"bwi-brush",
"bwi-bug",
"bwi-business",
"bwi-camera",
"bwi-check",
"bwi-check-circle",
"bwi-cli",
"bwi-clock",
"bwi-clone",
"bwi-close",
"bwi-cog",
"bwi-cog-f",
"bwi-collection",
"bwi-collection-shared",
"bwi-credit-card",
"bwi-dashboard",
"bwi-desktop",
"bwi-dollar",
"bwi-down-solid",
"bwi-download",
"bwi-drag-and-drop",
"bwi-ellipsis-h",
"bwi-ellipsis-v",
"bwi-envelope",
"bwi-error",
"bwi-exclamation-triangle",
"bwi-external-link",
"bwi-eye",
"bwi-eye-slash",
"bwi-family",
"bwi-file",
"bwi-file-text",
"bwi-files",
"bwi-filter",
"bwi-folder",
"bwi-generate",
"bwi-globe",
"bwi-hashtag",
"bwi-id-card",
"bwi-import",
"bwi-info-circle",
"bwi-key",
"bwi-list",
"bwi-list-alt",
"bwi-lock",
"bwi-lock-encrypted",
"bwi-lock-f",
"bwi-minus-circle",
"bwi-mobile",
"bwi-msp",
"bwi-numbered-list",
"bwi-paperclip",
"bwi-passkey",
"bwi-paypal",
"bwi-pencil",
"bwi-pencil-square",
"bwi-plus",
"bwi-plus-circle",
"bwi-popout",
"bwi-provider",
"bwi-puzzle",
"bwi-question-circle",
"bwi-refresh",
"bwi-search",
"bwi-send",
"bwi-share",
"bwi-shield",
"bwi-sign-in",
"bwi-sign-out",
"bwi-sliders",
"bwi-spinner",
"bwi-star",
"bwi-star-f",
"bwi-sticky-note",
"bwi-tag",
"bwi-trash",
"bwi-undo",
"bwi-universal-access",
"bwi-unlock",
"bwi-up-down-btn",
"bwi-up-solid",
"bwi-user",
"bwi-user-monitor",
"bwi-users",
"bwi-vault",
"bwi-wireless",
"bwi-wrench",
] as const;
/**
* Type-safe icon names derived from the BITWARDEN_ICONS array.
* This ensures type safety while allowing runtime iteration and validation.
*/
export type BitwardenIcon = (typeof BITWARDEN_ICONS)[number];