mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +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:
@@ -5579,17 +5579,37 @@
|
||||
"hasItemsVaultNudgeTitle": {
|
||||
"message": "Welcome to your vault!"
|
||||
},
|
||||
"phishingPageTitle":{
|
||||
"message": "Phishing website"
|
||||
"phishingPageTitleV2":{
|
||||
"message": "Phishing attempt detected"
|
||||
},
|
||||
"phishingPageCloseTab": {
|
||||
"message": "Close tab"
|
||||
"phishingPageSummary": {
|
||||
"message": "The site you are attempting to visit is a known malicious site and a security risk."
|
||||
},
|
||||
"phishingPageContinue": {
|
||||
"message": "Continue"
|
||||
"phishingPageCloseTabV2": {
|
||||
"message": "Close this tab"
|
||||
},
|
||||
"phishingPageLearnWhy": {
|
||||
"message": "Why are you seeing this?"
|
||||
"phishingPageContinueV2": {
|
||||
"message": "Continue to this site (not recommended)"
|
||||
},
|
||||
"phishingPageExplanation1": {
|
||||
"message": "This site was found in ",
|
||||
"description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name follows this."
|
||||
},
|
||||
"phishingPageExplanation2": {
|
||||
"message": ", an open-source list of known phishing sites used for stealing personal and sensitive information.",
|
||||
"description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name precedes this."
|
||||
},
|
||||
"phishingPageLearnMore" : {
|
||||
"message": "Learn more about phishing detection"
|
||||
},
|
||||
"protectedBy": {
|
||||
"message": "Protected by $PRODUCT$",
|
||||
"placeholders": {
|
||||
"product": {
|
||||
"content": "$1",
|
||||
"example": "Bitwarden Phishing Blocker"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hasItemsVaultNudgeBodyOne": {
|
||||
"message": "Autofill items for the current page"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<span>{{ "phishingPageLearnWhy"| i18n}}</span>
|
||||
<a href="http://bitwarden.com/help/phishing-blocked/" bitLink block buttonType="primary">
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
@@ -1,13 +1,46 @@
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "phishingPageTitle" | i18n }}</bit-label>
|
||||
<input bitInput disabled type="text" [value]="phishingHost" />
|
||||
</bit-form-field>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<div class="tw-flex tw-gap-4 tw-items-baseline">
|
||||
<bit-icon-tile size="large" icon="bwi-exclamation-triangle" variant="danger"></bit-icon-tile>
|
||||
<h1 bitTypography="h2" noMargin class="!tw-mb-0">{{ "phishingPageTitleV2" | i18n }}</h1>
|
||||
</div>
|
||||
|
||||
<button type="button" (click)="closeTab()" bitButton buttonType="primary">
|
||||
{{ "phishingPageCloseTab" | i18n }}
|
||||
</button>
|
||||
<button type="button" (click)="continueAnyway()" bitButton buttonType="danger">
|
||||
{{ "phishingPageContinue" | i18n }}
|
||||
</button>
|
||||
<hr class="!tw-mt-6 !tw-mb-4 !tw-border-secondary-100" />
|
||||
|
||||
<p bitTypography="body1">{{ "phishingPageSummary" | i18n }}</p>
|
||||
|
||||
<bit-callout class="tw-mb-0" type="danger" icon="bwi-globe" [title]="null">
|
||||
<span class="tw-font-mono">{{ phishingHost$ | async }}</span>
|
||||
</bit-callout>
|
||||
|
||||
<bit-callout class="tw-mt-2" [icon]="null" type="default">
|
||||
<p bitTypography="body2">
|
||||
{{ "phishingPageExplanation1" | i18n }}<b>Phishing.Database</b
|
||||
>{{ "phishingPageExplanation2" | i18n }}
|
||||
</p>
|
||||
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
href="https://bitwarden.com/help/phishing-blocked/"
|
||||
>
|
||||
{{ "phishingPageLearnMore" | i18n }}<i class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-callout>
|
||||
|
||||
<div class="tw-flex tw-flex-col tw-gap-4 tw-items-center tw-mt-2">
|
||||
<button type="button" (click)="closeTab()" bitButton buttonType="primary" [block]="true">
|
||||
{{ "phishingPageCloseTabV2" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
class="tw-text-sm"
|
||||
type="button"
|
||||
(click)="continueAnyway()"
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
>
|
||||
{{ "phishingPageContinueV2" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CommonModule } from "@angular/common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
import { Component, inject } from "@angular/core";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -13,12 +13,16 @@ import {
|
||||
CheckboxModule,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
IconTileComponent,
|
||||
LinkModule,
|
||||
CalloutComponent,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PhishingDetectionService } from "../services/phishing-detection.service";
|
||||
|
||||
@Component({
|
||||
selector: "dirt-phishing-warning",
|
||||
standalone: true,
|
||||
templateUrl: "phishing-warning.component.html",
|
||||
imports: [
|
||||
@@ -31,18 +35,16 @@ import { PhishingDetectionService } from "../services/phishing-detection.service
|
||||
CheckboxModule,
|
||||
ButtonModule,
|
||||
RouterModule,
|
||||
IconTileComponent,
|
||||
CalloutComponent,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class PhishingWarning implements OnDestroy {
|
||||
phishingHost = "";
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(private activatedRoute: ActivatedRoute) {
|
||||
this.activatedRoute.queryParamMap.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
||||
this.phishingHost = params.get("phishingHost") || "";
|
||||
});
|
||||
}
|
||||
export class PhishingWarning {
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
protected phishingHost$ = this.activatedRoute.queryParamMap.pipe(
|
||||
map((params) => params.get("phishingHost") || ""),
|
||||
);
|
||||
|
||||
async closeTab() {
|
||||
await PhishingDetectionService.requestClosePhishingWarningPage();
|
||||
@@ -50,9 +52,4 @@ export class PhishingWarning implements OnDestroy {
|
||||
async continueAnyway() {
|
||||
await PhishingDetectionService.requestContinueToDangerousUrl();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// TODO: This needs to be dealt with by moving this folder or updating the lint rule.
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { DeactivatedOrg } from "@bitwarden/assets/svg";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { AnonLayoutComponent, I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { PhishingWarning } from "./phishing-warning.component";
|
||||
import { ProtectedByComponent } from "./protected-by-component";
|
||||
|
||||
class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
|
||||
getApplicationVersion = () => Promise.resolve("Version 2024.1.1");
|
||||
getClientType = () => ClientType.Web;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create ActivatedRoute mock with query parameters
|
||||
*/
|
||||
function mockActivatedRoute(queryParams: Record<string, string>) {
|
||||
return {
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
queryParamMap: of({
|
||||
get: (key: string) => queryParams[key] || null,
|
||||
}),
|
||||
queryParams: of(queryParams),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type StoryArgs = {
|
||||
phishingHost: string;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Browser/DIRT/Phishing Warning",
|
||||
component: PhishingWarning,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [AnonLayoutComponent, ProtectedByComponent, RouterModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useClass: MockPlatformUtilsService,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () =>
|
||||
new I18nMockService({
|
||||
accessing: "Accessing",
|
||||
appLogoLabel: "Bitwarden logo",
|
||||
phishingPageTitleV2: "Phishing attempt detected",
|
||||
phishingPageCloseTabV2: "Close this tab",
|
||||
phishingPageSummary:
|
||||
"The site you are attempting to visit is a known malicious site and a security risk.",
|
||||
phishingPageContinueV2: "Continue to this site (not recommended)",
|
||||
phishingPageExplanation1: "This site was found in ",
|
||||
phishingPageExplanation2:
|
||||
", an open-source list of known phishing sites used for stealing personal and sensitive information.",
|
||||
phishingPageLearnMore: "Learn more about phishing detection",
|
||||
protectedBy: (product) => `Protected by ${product}`,
|
||||
learnMore: "Learn more",
|
||||
danger: "error",
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {
|
||||
environment$: new BehaviorSubject({
|
||||
getHostname() {
|
||||
return "bitwarden.com";
|
||||
},
|
||||
}).asObservable(),
|
||||
},
|
||||
},
|
||||
mockActivatedRoute({ phishingHost: "malicious-example.com" }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<auth-anon-layout
|
||||
[hideIcon]="true"
|
||||
[hideBackgroundIllustration]="true"
|
||||
>
|
||||
<dirt-phishing-warning></dirt-phishing-warning>
|
||||
<dirt-phishing-protected-by slot="secondary"></dirt-phishing-protected-by>
|
||||
</auth-anon-layout>
|
||||
`,
|
||||
}),
|
||||
argTypes: {
|
||||
phishingHost: {
|
||||
control: "text",
|
||||
description: "The suspicious host that was blocked",
|
||||
},
|
||||
},
|
||||
args: {
|
||||
phishingHost: "malicious-example.com",
|
||||
pageIcon: DeactivatedOrg,
|
||||
},
|
||||
} satisfies Meta<StoryArgs & { pageIcon: any }>;
|
||||
|
||||
type Story = StoryObj<StoryArgs & { pageIcon: any }>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
phishingHost: "malicious-example.com",
|
||||
},
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [mockActivatedRoute({ phishingHost: "malicious-example.com" })],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export const LongHostname: Story = {
|
||||
args: {
|
||||
phishingHost: "very-long-suspicious-phishing-domain-name-that-might-wrap.malicious-example.com",
|
||||
},
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
mockActivatedRoute({
|
||||
phishingHost:
|
||||
"very-long-suspicious-phishing-domain-name-that-might-wrap.malicious-example.com",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<span class="tw-text-muted">{{ "protectedBy" | i18n: "Bitwarden Phishing Blocker" }}</span>
|
||||
@@ -4,13 +4,12 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ButtonModule } from "@bitwarden/components";
|
||||
import { ButtonModule, LinkModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "dirt-phishing-protected-by",
|
||||
standalone: true,
|
||||
templateUrl: "learn-more-component.html",
|
||||
imports: [CommonModule, CommonModule, JslibModule, ButtonModule],
|
||||
templateUrl: "protected-by-component.html",
|
||||
imports: [CommonModule, CommonModule, JslibModule, ButtonModule, LinkModule],
|
||||
})
|
||||
export class LearnMoreComponent {
|
||||
constructor() {}
|
||||
}
|
||||
export class ProtectedByComponent {}
|
||||
@@ -116,15 +116,15 @@ export class PhishingDetectionService {
|
||||
/**
|
||||
* Sends a message to the phishing detection service to close the warning page
|
||||
*/
|
||||
static requestClosePhishingWarningPage(): void {
|
||||
void chrome.runtime.sendMessage({ command: PhishingDetectionMessage.Close });
|
||||
static async requestClosePhishingWarningPage() {
|
||||
await chrome.runtime.sendMessage({ command: PhishingDetectionMessage.Close });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the phishing detection service to continue to the caught url
|
||||
*/
|
||||
static async requestContinueToDangerousUrl() {
|
||||
void chrome.runtime.sendMessage({ command: PhishingDetectionMessage.Continue });
|
||||
await chrome.runtime.sendMessage({ command: PhishingDetectionMessage.Continue });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
VaultIcon,
|
||||
LockIcon,
|
||||
TwoFactorAuthSecurityKeyIcon,
|
||||
DeactivatedOrg,
|
||||
} from "@bitwarden/assets/svg";
|
||||
import {
|
||||
LoginComponent,
|
||||
@@ -54,8 +53,8 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma
|
||||
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
|
||||
import { LearnMoreComponent } from "../dirt/phishing-detection/pages/learn-more-component";
|
||||
import { PhishingWarning } from "../dirt/phishing-detection/pages/phishing-warning.component";
|
||||
import { ProtectedByComponent } from "../dirt/phishing-detection/pages/protected-by-component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import BrowserPopupUtils from "../platform/browser/browser-popup-utils";
|
||||
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
|
||||
@@ -718,14 +717,13 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: LearnMoreComponent,
|
||||
component: ProtectedByComponent,
|
||||
outlet: "secondary",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageIcon: DeactivatedOrg,
|
||||
pageTitle: "Bitwarden blocked it!",
|
||||
pageSubtitle: "Bitwarden blocked a known phishing site from loading.",
|
||||
hideIcon: true,
|
||||
hideBackgroundIllustration: true,
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
|
||||
@@ -382,7 +382,7 @@ app-root {
|
||||
}
|
||||
}
|
||||
|
||||
main:not(popup-page main) {
|
||||
main:not(popup-page main):not(auth-anon-layout main) {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
bottom: 0;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[maxWidth]="maxWidth"
|
||||
[hideCardWrapper]="hideCardWrapper"
|
||||
[hideIcon]="hideIcon"
|
||||
[hideBackgroundIllustration]="hideBackgroundIllustration"
|
||||
>
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet slot="secondary" name="secondary"></router-outlet>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
7
libs/components/src/icon-tile/icon-tile.component.html
Normal file
7
libs/components/src/icon-tile/icon-tile.component.html
Normal 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>
|
||||
111
libs/components/src/icon-tile/icon-tile.component.ts
Normal file
111
libs/components/src/icon-tile/icon-tile.component.ts
Normal 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];
|
||||
});
|
||||
}
|
||||
114
libs/components/src/icon-tile/icon-tile.stories.ts
Normal file
114
libs/components/src/icon-tile/icon-tile.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
1
libs/components/src/icon-tile/index.ts
Normal file
1
libs/components/src/icon-tile/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./icon-tile.component";
|
||||
@@ -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";
|
||||
|
||||
110
libs/components/src/shared/icon.ts
Normal file
110
libs/components/src/shared/icon.ts
Normal 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];
|
||||
Reference in New Issue
Block a user