mirror of
https://github.com/bitwarden/browser
synced 2026-02-19 19:04:01 +00:00
[CL-82] rename bit-icon to bit-svg; create new bit-icon component for font icons (#18584)
* rename bit-icon to bit-svg; create new bit-icon for font icons Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * find and replace current usage Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * add custom eslint warning Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix incorrect usage * fix tests * fix tests * Update libs/components/src/svg/index.ts Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Update libs/eslint/components/no-bwi-class-usage.spec.mjs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * update component api * update class name * use icon type in iconButton component * update type Icon --> BitSvg * fix bad renames * fix more renames * fix bad input * revert iconButton type * fix lint * fix more inputs * misc fixes Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix test * add eslint ignore * fix lint * add comparison story --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
committed by
jaasen-livefront
parent
67ff1e1d85
commit
de2f4a04fc
@@ -3,7 +3,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
|
||||
import { Subject, filter, of, switchMap, tap } from "rxjs";
|
||||
|
||||
import { Icon } from "@bitwarden/assets/svg";
|
||||
import { BitSvg } from "@bitwarden/assets/svg";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { Translation } from "../dialog";
|
||||
@@ -27,7 +27,7 @@ export interface AnonLayoutWrapperData {
|
||||
/**
|
||||
* The icon to display on the page. Pass null to hide the icon.
|
||||
*/
|
||||
pageIcon: Icon | null;
|
||||
pageIcon: BitSvg | null;
|
||||
/**
|
||||
* Optional flag to either show the optional environment selector (false) or just a readonly hostname (true).
|
||||
*/
|
||||
@@ -57,7 +57,7 @@ export class AnonLayoutWrapperComponent implements OnInit {
|
||||
|
||||
protected pageTitle?: string | null;
|
||||
protected pageSubtitle?: string | null;
|
||||
protected pageIcon: Icon | null = null;
|
||||
protected pageIcon: BitSvg | null = null;
|
||||
protected showReadonlyHostname?: boolean | null;
|
||||
protected maxWidth?: LandingContentMaxWidthType | null;
|
||||
protected hideCardWrapper?: boolean | null;
|
||||
|
||||
@@ -11,15 +11,15 @@ import {
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { BitwardenLogo, Icon } from "@bitwarden/assets/svg";
|
||||
import { BitwardenLogo, BitSvg } from "@bitwarden/assets/svg";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { IconModule } from "../icon";
|
||||
import { LandingContentMaxWidthType } from "../landing-layout";
|
||||
import { LandingLayoutModule } from "../landing-layout/landing-layout.module";
|
||||
import { SharedModule } from "../shared";
|
||||
import { SvgModule } from "../svg";
|
||||
import { TypographyModule } from "../typography";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
@@ -28,7 +28,7 @@ import { TypographyModule } from "../typography";
|
||||
selector: "auth-anon-layout",
|
||||
templateUrl: "./anon-layout.component.html",
|
||||
imports: [
|
||||
IconModule,
|
||||
SvgModule,
|
||||
CommonModule,
|
||||
TypographyModule,
|
||||
SharedModule,
|
||||
@@ -45,7 +45,7 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
|
||||
readonly title = input<string>();
|
||||
readonly subtitle = input<string>();
|
||||
readonly icon = model.required<Icon | null>();
|
||||
readonly icon = model.required<BitSvg | null>();
|
||||
readonly showReadonlyHostname = input<boolean>(false);
|
||||
readonly hideLogo = input<boolean>(false);
|
||||
readonly hideFooter = input<boolean>(false);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { Icon, LockIcon } from "@bitwarden/assets/svg";
|
||||
import { BitSvg, LockIcon } 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";
|
||||
@@ -23,7 +23,7 @@ type StoryArgs = AnonLayoutComponent & {
|
||||
contentLength: "normal" | "long" | "thin";
|
||||
showSecondary: boolean;
|
||||
useDefaultIcon: boolean;
|
||||
icon: Icon;
|
||||
icon: BitSvg;
|
||||
includeHeaderActions: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LinkModule, IconModule } from "@bitwarden/components";
|
||||
import { LinkModule, SvgModule } from "@bitwarden/components";
|
||||
|
||||
import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
component: CalloutComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [LinkModule, IconModule],
|
||||
imports: [LinkModule, SvgModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
BreadcrumbsModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
InputModule,
|
||||
MenuModule,
|
||||
NavigationModule,
|
||||
@@ -40,7 +40,7 @@ export default {
|
||||
BreadcrumbsModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
InputModule,
|
||||
MenuModule,
|
||||
NavigationModule,
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
import { Component, effect, input } from "@angular/core";
|
||||
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
|
||||
import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core";
|
||||
|
||||
import { Icon, isIcon } from "@bitwarden/assets/svg";
|
||||
import { BitwardenIcon } from "../shared/icon";
|
||||
|
||||
// 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-icon",
|
||||
standalone: true,
|
||||
host: {
|
||||
"[attr.aria-hidden]": "!ariaLabel()",
|
||||
"[class]": "classList()",
|
||||
"[attr.aria-hidden]": "ariaLabel() ? null : true",
|
||||
"[attr.aria-label]": "ariaLabel()",
|
||||
"[innerHtml]": "innerHtml",
|
||||
class: "tw-max-h-full tw-flex tw-justify-center",
|
||||
},
|
||||
template: ``,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BitIconComponent {
|
||||
innerHtml: SafeHtml | null = null;
|
||||
|
||||
readonly icon = input<Icon>();
|
||||
export class IconComponent {
|
||||
/**
|
||||
* The Bitwarden icon name (e.g., "bwi-lock", "bwi-user")
|
||||
*/
|
||||
readonly name = input.required<BitwardenIcon>();
|
||||
|
||||
/**
|
||||
* Accessible label for the icon
|
||||
*/
|
||||
readonly ariaLabel = input<string>();
|
||||
|
||||
constructor(private domSanitizer: DomSanitizer) {
|
||||
effect(() => {
|
||||
const icon = this.icon();
|
||||
if (!isIcon(icon)) {
|
||||
return;
|
||||
}
|
||||
const svg = icon.svg;
|
||||
this.innerHtml = this.domSanitizer.bypassSecurityTrustHtml(svg);
|
||||
});
|
||||
}
|
||||
protected readonly classList = computed(() => {
|
||||
return ["bwi", this.name()].join(" ");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,113 +8,40 @@ import * as stories from "./icon.stories";
|
||||
import { IconModule } from "@bitwarden/components";
|
||||
```
|
||||
|
||||
# Icon Use Instructions
|
||||
# Icon
|
||||
|
||||
- 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.
|
||||
The `bit-icon` component renders Bitwarden Web Icons (bwi) using icon font classes.
|
||||
|
||||
## Developer Instructions
|
||||
## Basic Usage
|
||||
|
||||
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.
|
||||
```html
|
||||
<bit-icon name="bwi-lock"></bit-icon>
|
||||
```
|
||||
|
||||
2. **Rename the file** as a `<name>.icon.ts` TypeScript file and place it in the `libs/assets/svg`
|
||||
lib.
|
||||
## Icon Names
|
||||
|
||||
3. **Import** `svgIcon` from `./icon-service`.
|
||||
All available icon names are defined in the `BitwardenIcon` type. Icons use the `bwi-*` naming
|
||||
convention (e.g., `bwi-lock`, `bwi-user`, `bwi-key`).
|
||||
|
||||
4. **Define and export** a `const` to represent your `svgIcon`.
|
||||
## Accessibility
|
||||
|
||||
```typescript
|
||||
export const ExampleIcon = svgIcon`<svg … </svg>`;
|
||||
```
|
||||
By default, icons are decorative and marked with `aria-hidden="true"`. To make an icon accessible,
|
||||
provide an `ariaLabel`:
|
||||
|
||||
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.
|
||||
```html
|
||||
<bit-icon name="bwi-lock" [ariaLabel]="'Secure lock'"></bit-icon>
|
||||
```
|
||||
|
||||
- A non-comprehensive list of common colors and their associated classes is below:
|
||||
## Styling
|
||||
|
||||
| Hardcoded Value | Tailwind Stroke Class | Tailwind Fill Class | Tailwind Variable |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----------------------------------- | ----------------------------------- |
|
||||
| `#020F66` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#020F66"}}></span> | `tw-stroke-illustration-outline` | `tw-fill-illustration-outline` | `--color-illustration-outline` |
|
||||
| `#DBE5F6` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#DBE5F6"}}></span> | `tw-stroke-illustration-bg-primary` | `tw-fill-illustration-bg-primary` | `--color-illustration-bg-primary` |
|
||||
| `#AAC3EF` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#AAC3EF"}}></span> | `tw-stroke-illustration-bg-secondary` | `tw-fill-illustration-bg-secondary` | `--color-illustration-bg-secondary` |
|
||||
| `#FFFFFF` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#FFFFFF"}}></span> | `tw-stroke-illustration-bg-tertiary` | `tw-fill-illustration-bg-tertiary` | `--color-illustration-bg-tertiary` |
|
||||
| `#FFBF00` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#FFBF00"}}></span> | `tw-stroke-illustration-tertiary` | `tw-fill-illustration-tertiary` | `--color-illustration-tertiary` |
|
||||
| `#175DDC` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#175DDC"}}></span> | `tw-stroke-illustration-logo` | `tw-fill-illustration-logo` | `--color-illustration-logo` |
|
||||
The component renders as an inline element. Apply standard CSS classes or styles to customize
|
||||
appearance:
|
||||
|
||||
- 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`.
|
||||
```html
|
||||
<bit-icon name="bwi-lock" class="tw-text-primary-500 tw-text-2xl"></bit-icon>
|
||||
```
|
||||
|
||||
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`.
|
||||
## Note on SVG Icons
|
||||
|
||||
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 { IconModule } from '@bitwarden/components';
|
||||
import { ExampleIcon, Example2Icon } from "@bitwarden/assets/svg";
|
||||
|
||||
@Component({
|
||||
selector: "app-example",
|
||||
standalone: true,
|
||||
imports: [IconModule],
|
||||
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 `<bit-icon>` component
|
||||
|
||||
```html
|
||||
<bit-icon [icon]="Icons.ExampleIcon"></bit-icon>
|
||||
```
|
||||
|
||||
With `ariaLabel`
|
||||
|
||||
```html
|
||||
<bit-icon [icon]="Icons.ExampleIcon" [ariaLabel]="Your custom label text here"></bit-icon>
|
||||
```
|
||||
|
||||
9. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client
|
||||
which supports multiple style modes.
|
||||
For SVG illustrations (not font icons), use the `bit-svg` component instead. See the Svg component
|
||||
documentation for details.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { BitIconComponent } from "./icon.component";
|
||||
import { IconComponent } from "./icon.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [BitIconComponent],
|
||||
exports: [BitIconComponent],
|
||||
imports: [IconComponent],
|
||||
exports: [IconComponent],
|
||||
})
|
||||
export class IconModule {}
|
||||
|
||||
@@ -1,50 +1,61 @@
|
||||
import { Meta } from "@storybook/angular";
|
||||
import { Meta, StoryObj } from "@storybook/angular";
|
||||
|
||||
import * as SvgIcons from "@bitwarden/assets/svg";
|
||||
import { BITWARDEN_ICONS } from "../shared/icon";
|
||||
|
||||
import { BitIconComponent } from "./icon.component";
|
||||
import { IconComponent } from "./icon.component";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Icon",
|
||||
component: BitIconComponent,
|
||||
component: IconComponent,
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-50335&t=k6OTDDPZOTtypRqo-11",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
argTypes: {
|
||||
name: {
|
||||
control: { type: "select" },
|
||||
options: BITWARDEN_ICONS,
|
||||
},
|
||||
},
|
||||
} as Meta<IconComponent>;
|
||||
|
||||
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
|
||||
isIcon,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
svgIcon,
|
||||
...Icons
|
||||
}: {
|
||||
[key: string]: any;
|
||||
} = SvgIcons;
|
||||
type Story = StoryObj<IconComponent>;
|
||||
|
||||
export const Default = {
|
||||
render: (args: { icons: [string, any][] }) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<div class="tw-bg-secondary-100 tw-p-2 tw-grid tw-grid-cols-[repeat(auto-fit,minmax(224px,1fr))] tw-gap-2">
|
||||
@for (icon of icons; track icon[0]) {
|
||||
<div class="tw-size-56 tw-border tw-border-secondary-300 tw-rounded-md">
|
||||
<div class="tw-text-xs tw-text-center">{{icon[0]}}</div>
|
||||
<div class="tw-size-52 tw-w-full tw-content-center">
|
||||
<bit-icon [icon]="icon[1]"></bit-icon>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
icons: Object.entries(Icons),
|
||||
name: "bwi-lock",
|
||||
},
|
||||
};
|
||||
|
||||
export const AllIcons: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<div class="tw-grid tw-grid-cols-[repeat(auto-fit,minmax(150px,1fr))] tw-gap-4 tw-p-4">
|
||||
@for (icon of icons; track icon) {
|
||||
<div class="tw-flex tw-flex-col tw-items-center tw-p-2 tw-border tw-border-secondary-300 tw-rounded">
|
||||
<bit-icon [name]="icon" class="tw-text-2xl tw-mb-2"></bit-icon>
|
||||
<span class="tw-text-xs tw-text-center">{{ icon }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
props: {
|
||||
icons: BITWARDEN_ICONS,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithAriaLabel: Story = {
|
||||
args: {
|
||||
name: "bwi-lock",
|
||||
ariaLabel: "Secure lock icon",
|
||||
},
|
||||
};
|
||||
|
||||
export const CompareWithLegacy: Story = {
|
||||
render: () => ({
|
||||
template: `<bit-icon name="bwi-lock"></bit-icon> <i class="bwi bwi-lock"></i>`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./icon.module";
|
||||
export * from "./icon.component";
|
||||
|
||||
@@ -22,6 +22,7 @@ export * from "./form-field";
|
||||
export * from "./header";
|
||||
export * from "./icon-button";
|
||||
export * from "./icon";
|
||||
export * from "./svg";
|
||||
export * from "./icon-tile";
|
||||
export * from "./input";
|
||||
export * from "./item";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[routerLink]="['/']"
|
||||
class="tw-w-32 tw-py-5 sm:tw-w-[200px] tw-self-center sm:tw-self-start tw-block [&>*]:tw-align-top"
|
||||
>
|
||||
<bit-icon [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
|
||||
<bit-svg [content]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-svg>
|
||||
</a>
|
||||
}
|
||||
<div class="[&:has(*)]:tw-ms-auto [&:has(*)]:tw-py-5">
|
||||
|
||||
@@ -3,8 +3,8 @@ import { RouterModule } from "@angular/router";
|
||||
|
||||
import { BitwardenLogo } from "@bitwarden/assets/svg";
|
||||
|
||||
import { IconModule } from "../icon";
|
||||
import { SharedModule } from "../shared";
|
||||
import { SvgModule } from "../svg";
|
||||
|
||||
/**
|
||||
* Header component for landing pages with optional Bitwarden logo and header actions slot.
|
||||
@@ -34,7 +34,7 @@ import { SharedModule } from "../shared";
|
||||
selector: "bit-landing-header",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "./landing-header.component.html",
|
||||
imports: [RouterModule, IconModule, SharedModule],
|
||||
imports: [RouterModule, SvgModule, SharedModule],
|
||||
})
|
||||
export class LandingHeaderComponent {
|
||||
readonly hideLogo = input<boolean>(false);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div
|
||||
class="tw-size-20 sm:tw-size-24 [&_svg]:tw-w-full [&_svg]:tw-max-w-24 tw-mx-auto tw-content-center"
|
||||
>
|
||||
<bit-icon [icon]="icon()"></bit-icon>
|
||||
<bit-svg [content]="icon()"></bit-svg>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
|
||||
|
||||
import { Icon } from "@bitwarden/assets/svg";
|
||||
import { BitSvg } from "@bitwarden/assets/svg";
|
||||
|
||||
import { IconModule } from "../icon";
|
||||
import { SvgModule } from "../svg";
|
||||
import { TypographyModule } from "../typography";
|
||||
|
||||
/**
|
||||
@@ -31,10 +31,10 @@ import { TypographyModule } from "../typography";
|
||||
selector: "bit-landing-hero",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "./landing-hero.component.html",
|
||||
imports: [IconModule, TypographyModule],
|
||||
imports: [SvgModule, TypographyModule],
|
||||
})
|
||||
export class LandingHeroComponent {
|
||||
readonly icon = input<Icon | null>(null);
|
||||
readonly icon = input<BitSvg | null>(null);
|
||||
readonly title = input<string | undefined>();
|
||||
readonly subtitle = input<string | undefined>();
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
<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>
|
||||
<bit-svg [content]="leftIllustration"></bit-svg>
|
||||
</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>
|
||||
<bit-svg [content]="rightIllustration"></bit-svg>
|
||||
</div>
|
||||
}
|
||||
<ng-content select="bit-landing-footer"></ng-content>
|
||||
|
||||
@@ -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<boolean>(false);
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
routerLinkActive
|
||||
ariaCurrentWhenActive="page"
|
||||
>
|
||||
<bit-icon [icon]="sideNavService.open() ? openIcon() : closedIcon()"></bit-icon>
|
||||
<bit-svg [content]="sideNavService.open() ? openIcon() : closedIcon()"></bit-svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -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<Icon>();
|
||||
readonly openIcon = input.required<BitSvg>();
|
||||
|
||||
/**
|
||||
* Route to be passed to internal `routerLink`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="tw-mx-auto tw-flex tw-flex-col tw-items-center tw-justify-center tw-pt-6">
|
||||
<div class="tw-max-w-sm tw-flex tw-flex-col tw-items-center">
|
||||
<div class="tw-size-24 tw-content-center">
|
||||
<bit-icon [icon]="icon()" aria-hidden="true"></bit-icon>
|
||||
<bit-svg [content]="icon()" aria-hidden="true"></bit-svg>
|
||||
</div>
|
||||
<h3 class="tw-font-medium tw-text-center tw-mt-4">
|
||||
<ng-content select="[slot=title]"></ng-content>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
2
libs/components/src/svg/index.ts
Normal file
2
libs/components/src/svg/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./svg.module";
|
||||
export * from "./svg.component";
|
||||
31
libs/components/src/svg/svg.component.ts
Normal file
31
libs/components/src/svg/svg.component.ts
Normal file
@@ -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<BitSvg>();
|
||||
readonly ariaLabel = input<string>();
|
||||
|
||||
protected readonly innerHtml = computed<SafeHtml | null>(() => {
|
||||
const content = this.content();
|
||||
if (!isBitSvg(content)) {
|
||||
return null;
|
||||
}
|
||||
const svg = content.svg;
|
||||
return this.domSanitizer.bypassSecurityTrustHtml(svg);
|
||||
});
|
||||
}
|
||||
@@ -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<BitIconComponent>;
|
||||
describe("SvgComponent", () => {
|
||||
let fixture: ComponentFixture<SvgComponent>;
|
||||
|
||||
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`<svg><text x="0" y="15">safe icon</text></svg>`;
|
||||
const icon = svg`<svg><text x="0" y="15">safe icon</text></svg>`;
|
||||
|
||||
fixture.componentRef.setInput("icon", icon);
|
||||
fixture.componentRef.setInput("content", icon);
|
||||
fixture.detectChanges();
|
||||
|
||||
const el = fixture.nativeElement as HTMLElement;
|
||||
120
libs/components/src/svg/svg.mdx
Normal file
120
libs/components/src/svg/svg.mdx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Meta, Story, Controls } from "@storybook/addon-docs/blocks";
|
||||
|
||||
import * as stories from "./svg.stories";
|
||||
|
||||
<Meta of={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 `<name>.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`<svg … </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` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#020F66"}}></span> | `tw-stroke-illustration-outline` | `tw-fill-illustration-outline` | `--color-illustration-outline` |
|
||||
| `#DBE5F6` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#DBE5F6"}}></span> | `tw-stroke-illustration-bg-primary` | `tw-fill-illustration-bg-primary` | `--color-illustration-bg-primary` |
|
||||
| `#AAC3EF` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#AAC3EF"}}></span> | `tw-stroke-illustration-bg-secondary` | `tw-fill-illustration-bg-secondary` | `--color-illustration-bg-secondary` |
|
||||
| `#FFFFFF` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#FFFFFF"}}></span> | `tw-stroke-illustration-bg-tertiary` | `tw-fill-illustration-bg-tertiary` | `--color-illustration-bg-tertiary` |
|
||||
| `#FFBF00` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#FFBF00"}}></span> | `tw-stroke-illustration-tertiary` | `tw-fill-illustration-tertiary` | `--color-illustration-tertiary` |
|
||||
| `#175DDC` <span style={{ display: "inline-block", width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#175DDC"}}></span> | `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 `<bit-svg>` component
|
||||
|
||||
```html
|
||||
<bit-svg [content]="Icons.ExampleIcon"></bit-svg>
|
||||
```
|
||||
|
||||
With `ariaLabel`
|
||||
|
||||
```html
|
||||
<bit-svg [content]="Icons.ExampleIcon" [ariaLabel]="Your custom label text here"></bit-svg>
|
||||
```
|
||||
|
||||
9. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client
|
||||
which supports multiple style modes.
|
||||
9
libs/components/src/svg/svg.module.ts
Normal file
9
libs/components/src/svg/svg.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SvgComponent } from "./svg.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SvgComponent],
|
||||
exports: [SvgComponent],
|
||||
})
|
||||
export class SvgModule {}
|
||||
50
libs/components/src/svg/svg.stories.ts
Normal file
50
libs/components/src/svg/svg.stories.ts
Normal file
@@ -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*/ `
|
||||
<div class="tw-bg-secondary-100 tw-p-2 tw-grid tw-grid-cols-[repeat(auto-fit,minmax(224px,1fr))] tw-gap-2">
|
||||
@for (icon of icons; track icon[0]) {
|
||||
<div class="tw-size-56 tw-border tw-border-secondary-300 tw-rounded-md">
|
||||
<div class="tw-text-xs tw-text-center">{{icon[0]}}</div>
|
||||
<div class="tw-size-52 tw-w-full tw-content-center">
|
||||
<bit-svg [content]="icon[1]"></bit-svg>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
icons: Object.entries(Icons),
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user