}
diff --git a/libs/components/src/landing-layout/landing-layout.component.ts b/libs/components/src/landing-layout/landing-layout.component.ts
index 520cca945d6..65c7302e828 100644
--- a/libs/components/src/landing-layout/landing-layout.component.ts
+++ b/libs/components/src/landing-layout/landing-layout.component.ts
@@ -3,7 +3,7 @@ import { Component, ChangeDetectionStrategy, inject, input } from "@angular/core
import { BackgroundLeftIllustration, BackgroundRightIllustration } from "@bitwarden/assets/svg";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
-import { IconModule } from "../icon";
+import { SvgModule } from "../svg";
/**
* Root layout component for landing pages providing a full-screen container with optional decorative background illustrations.
@@ -27,7 +27,7 @@ import { IconModule } from "../icon";
selector: "bit-landing-layout",
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: "./landing-layout.component.html",
- imports: [IconModule],
+ imports: [SvgModule],
})
export class LandingLayoutComponent {
readonly hideBackgroundIllustration = input(false);
diff --git a/libs/components/src/navigation/nav-logo.component.html b/libs/components/src/navigation/nav-logo.component.html
index 9f18855ae13..8323a0f3479 100644
--- a/libs/components/src/navigation/nav-logo.component.html
+++ b/libs/components/src/navigation/nav-logo.component.html
@@ -16,6 +16,6 @@
routerLinkActive
ariaCurrentWhenActive="page"
>
-
+
diff --git a/libs/components/src/navigation/nav-logo.component.ts b/libs/components/src/navigation/nav-logo.component.ts
index fec50ee8902..4b3dc471edb 100644
--- a/libs/components/src/navigation/nav-logo.component.ts
+++ b/libs/components/src/navigation/nav-logo.component.ts
@@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, Component, input, inject } from "@angular/core";
import { RouterLinkActive, RouterLink } from "@angular/router";
-import { BitwardenShield, Icon } from "@bitwarden/assets/svg";
+import { BitwardenShield, BitSvg } from "@bitwarden/assets/svg";
-import { BitIconComponent } from "../icon/icon.component";
+import { SvgComponent } from "../svg/svg.component";
import { SideNavService } from "./side-nav.service";
@Component({
selector: "bit-nav-logo",
templateUrl: "./nav-logo.component.html",
- imports: [RouterLinkActive, RouterLink, BitIconComponent],
+ imports: [RouterLinkActive, RouterLink, SvgComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NavLogoComponent {
@@ -26,7 +26,7 @@ export class NavLogoComponent {
/**
* Icon that is displayed when the side nav is open
*/
- readonly openIcon = input.required();
+ readonly openIcon = input.required();
/**
* Route to be passed to internal `routerLink`
diff --git a/libs/components/src/no-items/no-items.component.html b/libs/components/src/no-items/no-items.component.html
index e728584a41a..46a5c25526a 100644
--- a/libs/components/src/no-items/no-items.component.html
+++ b/libs/components/src/no-items/no-items.component.html
@@ -1,7 +1,7 @@
-
+
diff --git a/libs/components/src/no-items/no-items.component.ts b/libs/components/src/no-items/no-items.component.ts
index c6e52a1f83d..d2cacfd2251 100644
--- a/libs/components/src/no-items/no-items.component.ts
+++ b/libs/components/src/no-items/no-items.component.ts
@@ -1,18 +1,17 @@
-import { Component, input } from "@angular/core";
+import { ChangeDetectionStrategy, Component, input } from "@angular/core";
import { NoResults } from "@bitwarden/assets/svg";
-import { BitIconComponent } from "../icon/icon.component";
+import { SvgComponent } from "../svg/svg.component";
/**
* Component for displaying a message when there are no items to display. Expects title, description and button slots.
*/
-// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
-// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "bit-no-items",
templateUrl: "./no-items.component.html",
- imports: [BitIconComponent],
+ imports: [SvgComponent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NoItemsComponent {
readonly icon = input(NoResults);
diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts
index c4fe2f9b2af..398251fd2e2 100644
--- a/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts
+++ b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts
@@ -16,7 +16,6 @@ import { DialogModule } from "../../dialog";
import { DrawerModule } from "../../drawer";
import { FormControlModule } from "../../form-control";
import { FormFieldModule } from "../../form-field";
-import { IconModule } from "../../icon";
import { IconButtonModule } from "../../icon-button";
import { InputModule } from "../../input";
import { LayoutComponent } from "../../layout";
@@ -31,6 +30,7 @@ import { SearchModule } from "../../search";
import { SectionComponent } from "../../section";
import { SelectModule } from "../../select";
import { SharedModule } from "../../shared";
+import { SvgModule } from "../../svg";
import { TableModule } from "../../table";
import { TabsModule } from "../../tabs";
import { ToggleGroupModule } from "../../toggle-group";
@@ -54,7 +54,7 @@ import { TypographyModule } from "../../typography";
FormFieldModule,
FormsModule,
IconButtonModule,
- IconModule,
+ SvgModule,
InputModule,
LayoutComponent,
LinkModule,
@@ -92,7 +92,7 @@ import { TypographyModule } from "../../typography";
FormFieldModule,
FormsModule,
IconButtonModule,
- IconModule,
+ SvgModule,
InputModule,
LayoutComponent,
LinkModule,
diff --git a/libs/components/src/svg/index.ts b/libs/components/src/svg/index.ts
new file mode 100644
index 00000000000..ae4c480e786
--- /dev/null
+++ b/libs/components/src/svg/index.ts
@@ -0,0 +1,2 @@
+export * from "./svg.module";
+export * from "./svg.component";
diff --git a/libs/components/src/svg/svg.component.ts b/libs/components/src/svg/svg.component.ts
new file mode 100644
index 00000000000..bcb63cfa568
--- /dev/null
+++ b/libs/components/src/svg/svg.component.ts
@@ -0,0 +1,31 @@
+import { ChangeDetectionStrategy, Component, computed, inject, input } from "@angular/core";
+import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
+
+import { BitSvg, isBitSvg } from "@bitwarden/assets/svg";
+
+@Component({
+ selector: "bit-svg",
+ host: {
+ "[attr.aria-hidden]": "!ariaLabel()",
+ "[attr.aria-label]": "ariaLabel()",
+ "[innerHtml]": "innerHtml()",
+ class: "tw-max-h-full tw-flex tw-justify-center",
+ },
+ template: ``,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SvgComponent {
+ private domSanitizer = inject(DomSanitizer);
+
+ readonly content = input();
+ readonly ariaLabel = input();
+
+ protected readonly innerHtml = computed(() => {
+ const content = this.content();
+ if (!isBitSvg(content)) {
+ return null;
+ }
+ const svg = content.svg;
+ return this.domSanitizer.bypassSecurityTrustHtml(svg);
+ });
+}
diff --git a/libs/components/src/icon/icon.components.spec.ts b/libs/components/src/svg/svg.components.spec.ts
similarity index 55%
rename from libs/components/src/icon/icon.components.spec.ts
rename to libs/components/src/svg/svg.components.spec.ts
index 3ae37ff5423..55874d29e6c 100644
--- a/libs/components/src/icon/icon.components.spec.ts
+++ b/libs/components/src/svg/svg.components.spec.ts
@@ -1,25 +1,25 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { Icon, svgIcon } from "@bitwarden/assets/svg";
+import { BitSvg, svg } from "@bitwarden/assets/svg";
-import { BitIconComponent } from "./icon.component";
+import { SvgComponent } from "./svg.component";
-describe("IconComponent", () => {
- let fixture: ComponentFixture;
+describe("SvgComponent", () => {
+ let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [BitIconComponent],
+ imports: [SvgComponent],
}).compileComponents();
- fixture = TestBed.createComponent(BitIconComponent);
+ fixture = TestBed.createComponent(SvgComponent);
fixture.detectChanges();
});
it("should have empty innerHtml when input is not an Icon", () => {
- const fakeIcon = { svg: "harmful user input" } as Icon;
+ const fakeIcon = { svg: "harmful user input" } as BitSvg;
- fixture.componentRef.setInput("icon", fakeIcon);
+ fixture.componentRef.setInput("content", fakeIcon);
fixture.detectChanges();
const el = fixture.nativeElement as HTMLElement;
@@ -27,9 +27,9 @@ describe("IconComponent", () => {
});
it("should contain icon when input is a safe Icon", () => {
- const icon = svgIcon`safe icon`;
+ const icon = svg`safe icon`;
- fixture.componentRef.setInput("icon", icon);
+ fixture.componentRef.setInput("content", icon);
fixture.detectChanges();
const el = fixture.nativeElement as HTMLElement;
diff --git a/libs/components/src/svg/svg.mdx b/libs/components/src/svg/svg.mdx
new file mode 100644
index 00000000000..a29a6f86b14
--- /dev/null
+++ b/libs/components/src/svg/svg.mdx
@@ -0,0 +1,120 @@
+import { Meta, Story, Controls } from "@storybook/addon-docs/blocks";
+
+import * as stories from "./svg.stories";
+
+
+
+```ts
+import { SvgModule } from "@bitwarden/components";
+```
+
+# Svg Use Instructions
+
+- Icons will generally be attached to the associated Jira task.
+ - Designers should minify any SVGs before attaching them to Jira using a tool like
+ [SVGOMG](https://jakearchibald.github.io/svgomg/).
+ - **Note:** Ensure the "Remove viewbox" option is toggled off if responsive resizing of the icon
+ is desired.
+
+## Developer Instructions
+
+1. **Download the SVG** and import it as an `.svg` initially into the IDE of your choice.
+ - The SVG should be formatted using either a built-in formatter or an external tool like
+ [SVG Formatter Beautifier](https://codebeautify.org/svg-formatter-beautifier) to make applying
+ classes easier.
+
+2. **Rename the file** as a `.icon.ts` TypeScript file and place it in the `libs/assets/svg`
+ lib.
+
+3. **Import** `svg` from `./svg`.
+
+4. **Define and export** a `const` to represent your `svg`.
+
+ ```typescript
+ export const ExampleIcon = svg``;
+ ```
+
+5. **Replace any hardcoded strokes or fills** with the appropriate Tailwind class.
+ - **Note:** Stroke is used when styling the outline of an SVG path, while fill is used when
+ styling the inside of an SVG path.
+
+ - A non-comprehensive list of common colors and their associated classes is below:
+
+ | Hardcoded Value | Tailwind Stroke Class | Tailwind Fill Class | Tailwind Variable |
+ | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----------------------------------- | ----------------------------------- |
+ | `#020F66` | `tw-stroke-illustration-outline` | `tw-fill-illustration-outline` | `--color-illustration-outline` |
+ | `#DBE5F6` | `tw-stroke-illustration-bg-primary` | `tw-fill-illustration-bg-primary` | `--color-illustration-bg-primary` |
+ | `#AAC3EF` | `tw-stroke-illustration-bg-secondary` | `tw-fill-illustration-bg-secondary` | `--color-illustration-bg-secondary` |
+ | `#FFFFFF` | `tw-stroke-illustration-bg-tertiary` | `tw-fill-illustration-bg-tertiary` | `--color-illustration-bg-tertiary` |
+ | `#FFBF00` | `tw-stroke-illustration-tertiary` | `tw-fill-illustration-tertiary` | `--color-illustration-tertiary` |
+ | `#175DDC` | `tw-stroke-illustration-logo` | `tw-fill-illustration-logo` | `--color-illustration-logo` |
+
+ - If the hex that you have on an SVG path is not listed above, there are a few ways to figure out
+ the appropriate Tailwind class:
+ - **Option 1: Figma**
+ - Open the SVG in Figma.
+ - Click on an individual path on the SVG until you see the path's properties in the
+ right-hand panel.
+ - Scroll down to the Colors section.
+ - Example: `Color/Illustration/Outline`
+ - This also includes Hex or RGB values that can be used to find the appropriate Tailwind
+ variable as well if you follow the manual search option below.
+ - Create the appropriate stroke or fill class from the color used.
+ - Example: `Color/Illustration/Outline` corresponds to `--color-illustration-outline` which
+ corresponds to `tw-stroke-illustration-outline` or `tw-fill-illustration-outline`.
+ - **Option 2: Manual Search**
+ - Take the path's stroke or fill hex value and convert it to RGB using a tool like
+ [Hex to RGB](https://www.rgbtohex.net/hex-to-rgb/).
+ - Search for the RGB value without commas in our `tw-theme.css` to find the Tailwind variable
+ that corresponds to the color.
+ - Create the appropriate stroke or fill class using the Tailwind variable.
+ - Example: `--color-illustration-outline` corresponds to `tw-stroke-illustration-outline`
+ or `tw-fill-illustration-outline`.
+
+6. **Remove any hardcoded width or height attributes** if your SVG has a configured
+ [viewBox](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox) attribute in order
+ to allow the SVG to scale to fit its container.
+ - **Note:** Scaling is required for any SVG used as an
+ [AnonLayout](?path=/docs/component-library-anon-layout--docs) `pageIcon`.
+
+7. **Replace any generic `clipPath` ids** (such as `id="a"`) with a unique id, and update the
+ referencing element to use the new id (such as `clip-path="url(#unique-id-here)"`).
+
+8. **Import your SVG const** anywhere you want to use the SVG.
+ - **Angular Component Example:**
+ - **TypeScript:**
+
+ ```typescript
+ import { Component } from "@angular/core";
+ import { SvgModule } from '@bitwarden/components';
+ import { ExampleIcon, Example2Icon } from "@bitwarden/assets/svg";
+
+ @Component({
+ selector: "app-example",
+ standalone: true,
+ imports: [SvgModule],
+ templateUrl: "./example.component.html",
+ })
+ export class ExampleComponent {
+ readonly Icons = { ExampleIcon, Example2Icon };
+ ...
+ }
+ ```
+
+ - **HTML:**
+
+ > NOTE: SVG icons are treated as decorative by default and will be `aria-hidden` unless an
+ > `ariaLabel` is explicitly provided to the `` component
+
+ ```html
+
+ ```
+
+ With `ariaLabel`
+
+ ```html
+
+ ```
+
+9. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client
+ which supports multiple style modes.
diff --git a/libs/components/src/svg/svg.module.ts b/libs/components/src/svg/svg.module.ts
new file mode 100644
index 00000000000..c1cdae0e232
--- /dev/null
+++ b/libs/components/src/svg/svg.module.ts
@@ -0,0 +1,9 @@
+import { NgModule } from "@angular/core";
+
+import { SvgComponent } from "./svg.component";
+
+@NgModule({
+ imports: [SvgComponent],
+ exports: [SvgComponent],
+})
+export class SvgModule {}
diff --git a/libs/components/src/svg/svg.stories.ts b/libs/components/src/svg/svg.stories.ts
new file mode 100644
index 00000000000..b2eb10771ce
--- /dev/null
+++ b/libs/components/src/svg/svg.stories.ts
@@ -0,0 +1,50 @@
+import { Meta } from "@storybook/angular";
+
+import * as SvgIcons from "@bitwarden/assets/svg";
+
+import { SvgComponent } from "./svg.component";
+
+export default {
+ title: "Component Library/Svg",
+ component: SvgComponent,
+ parameters: {
+ design: {
+ type: "figma",
+ url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-50335&t=k6OTDDPZOTtypRqo-11",
+ },
+ },
+} as Meta;
+
+const {
+ // Filtering out the few non-icons in the libs/assets/svg import
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ DynamicContentNotAllowedError: _DynamicContentNotAllowedError,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ isBitSvg,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ svg,
+ ...Icons
+}: {
+ [key: string]: any;
+} = SvgIcons;
+
+export const Default = {
+ render: (args: { icons: [string, any][] }) => ({
+ props: args,
+ template: /*html*/ `
+