+
+
`,
diff --git a/libs/components/src/dialog/simple-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog.service.stories.ts
index 90963515aa..ab973484c8 100644
--- a/libs/components/src/dialog/simple-dialog.service.stories.ts
+++ b/libs/components/src/dialog/simple-dialog.service.stories.ts
@@ -2,7 +2,12 @@ import { DialogModule, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
+import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
+
import { ButtonModule } from "../button";
+import { IconButtonModule } from "../icon-button";
+import { SharedModule } from "../shared/shared.module";
+import { I18nMockService } from "../utils/i18n-mock.service";
import { DialogService } from "./dialog.service";
import { DialogCloseDirective } from "./directives/dialog-close.directive";
@@ -60,13 +65,23 @@ export default {
decorators: [
moduleMetadata({
declarations: [
- DialogCloseDirective,
- SimpleDialogComponent,
- DialogTitleContainerDirective,
StoryDialogContentComponent,
+ DialogCloseDirective,
+ DialogTitleContainerDirective,
+ SimpleDialogComponent,
+ ],
+ imports: [SharedModule, IconButtonModule, ButtonModule, DialogModule],
+ providers: [
+ DialogService,
+ {
+ provide: I18nService,
+ useFactory: () => {
+ return new I18nMockService({
+ close: "Close",
+ });
+ },
+ },
],
- imports: [ButtonModule, DialogModule],
- providers: [DialogService],
}),
],
parameters: {
diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts
new file mode 100644
index 0000000000..cd6f1be402
--- /dev/null
+++ b/libs/components/src/icon-button/icon-button.component.ts
@@ -0,0 +1,115 @@
+import { Component, HostBinding, Input } from "@angular/core";
+
+export type IconButtonStyle = "contrast" | "main" | "muted" | "primary" | "secondary" | "danger";
+
+const styles: Record
= {
+ contrast: [
+ "tw-bg-transparent",
+ "!tw-text-contrast",
+ "tw-border-transparent",
+ "hover:tw-bg-transparent-hover",
+ "hover:tw-border-text-contrast",
+ "focus-visible:before:tw-ring-text-contrast",
+ "disabled:hover:tw-bg-transparent",
+ ],
+ main: [
+ "tw-bg-transparent",
+ "!tw-text-main",
+ "tw-border-transparent",
+ "hover:tw-bg-transparent-hover",
+ "hover:tw-border-text-main",
+ "focus-visible:before:tw-ring-text-main",
+ "disabled:hover:tw-bg-transparent",
+ ],
+ muted: [
+ "tw-bg-transparent",
+ "!tw-text-muted",
+ "tw-border-transparent",
+ "hover:tw-bg-transparent-hover",
+ "hover:tw-border-primary-700",
+ "focus-visible:before:tw-ring-primary-700",
+ "disabled:hover:tw-bg-transparent",
+ ],
+ primary: [
+ "tw-bg-primary-500",
+ "!tw-text-contrast",
+ "tw-border-primary-500",
+ "hover:tw-bg-primary-700",
+ "hover:tw-border-primary-700",
+ "focus-visible:before:tw-ring-primary-700",
+ "disabled:hover:tw-bg-primary-500",
+ ],
+ secondary: [
+ "tw-bg-transparent",
+ "!tw-text-muted",
+ "tw-border-text-muted",
+ "hover:!tw-text-contrast",
+ "hover:tw-bg-text-muted",
+ "focus-visible:before:tw-ring-primary-700",
+ "disabled:hover:tw-bg-transparent",
+ "disabled:hover:!tw-text-muted",
+ "disabled:hover:tw-border-text-muted",
+ ],
+ danger: [
+ "tw-bg-transparent",
+ "!tw-text-danger",
+ "tw-border-danger-500",
+ "hover:!tw-text-contrast",
+ "hover:tw-bg-danger-500",
+ "focus-visible:before:tw-ring-primary-700",
+ "disabled:hover:tw-bg-transparent",
+ "disabled:hover:!tw-text-danger",
+ "disabled:hover:tw-border-danger-500",
+ ],
+};
+
+export type IconButtonSize = "default" | "small";
+
+const sizes: Record = {
+ default: ["tw-px-2.5", "tw-py-1.5"],
+ small: ["tw-leading-none", "tw-text-base", "tw-p-1"],
+};
+
+@Component({
+ selector: "button[bitIconButton]",
+ template: ``,
+})
+export class BitIconButtonComponent {
+ @Input("bitIconButton") icon: string;
+
+ @Input() buttonType: IconButtonStyle = "main";
+
+ @Input() size: IconButtonSize = "default";
+
+ @HostBinding("class") get classList() {
+ return [
+ "tw-font-semibold",
+ "tw-border",
+ "tw-border-solid",
+ "tw-rounded",
+ "tw-transition",
+ "hover:tw-no-underline",
+ "disabled:tw-opacity-60",
+ "disabled:hover:tw-border-transparent",
+ "focus:tw-outline-none",
+
+ // Workaround for box-shadow with transparent offset issue:
+ // https://github.com/tailwindlabs/tailwindcss/issues/3595
+ // Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better:
+ // switch to `outline` with `outline-offset` when Safari supports border radius on outline.
+ // Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred.
+ "tw-relative",
+ "before:tw-content-['']",
+ "before:tw-block",
+ "before:tw-absolute",
+ "before:-tw-inset-[3px]",
+ "before:tw-rounded-md",
+ "before:tw-transition",
+ "before:tw-ring",
+ "focus-visible:before:tw-ring-text-contrast",
+ "focus-visible:tw-z-10",
+ ]
+ .concat(styles[this.buttonType])
+ .concat(sizes[this.size]);
+ }
+}
diff --git a/libs/components/src/icon-button/icon-button.module.ts b/libs/components/src/icon-button/icon-button.module.ts
new file mode 100644
index 0000000000..fb4e858971
--- /dev/null
+++ b/libs/components/src/icon-button/icon-button.module.ts
@@ -0,0 +1,11 @@
+import { CommonModule } from "@angular/common";
+import { NgModule } from "@angular/core";
+
+import { BitIconButtonComponent } from "./icon-button.component";
+
+@NgModule({
+ imports: [CommonModule],
+ declarations: [BitIconButtonComponent],
+ exports: [BitIconButtonComponent],
+})
+export class IconButtonModule {}
diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts
new file mode 100644
index 0000000000..7be45d3f71
--- /dev/null
+++ b/libs/components/src/icon-button/icon-button.stories.ts
@@ -0,0 +1,59 @@
+import { Meta, Story } from "@storybook/angular";
+
+import { BitIconButtonComponent } from "./icon-button.component";
+
+export default {
+ title: "Component Library/Icon Button",
+ component: BitIconButtonComponent,
+ args: {
+ bitIconButton: "bwi-plus",
+ buttonType: "primary",
+ size: "default",
+ disabled: false,
+ },
+} as Meta;
+
+const Template: Story = (args: BitIconButtonComponent) => ({
+ props: args,
+ template: `
+
+
+
+ `,
+});
+
+export const Contrast = Template.bind({});
+Contrast.args = {
+ buttonType: "contrast",
+};
+
+export const Main = Template.bind({});
+Main.args = {
+ buttonType: "main",
+};
+
+export const Muted = Template.bind({});
+Muted.args = {
+ buttonType: "muted",
+};
+
+export const Primary = Template.bind({});
+Primary.args = {
+ buttonType: "primary",
+};
+
+export const Secondary = Template.bind({});
+Secondary.args = {
+ buttonType: "secondary",
+};
+
+export const Danger = Template.bind({});
+Danger.args = {
+ buttonType: "danger",
+};
diff --git a/libs/components/src/icon-button/index.ts b/libs/components/src/icon-button/index.ts
new file mode 100644
index 0000000000..9da4a3162b
--- /dev/null
+++ b/libs/components/src/icon-button/index.ts
@@ -0,0 +1 @@
+export * from "./icon-button.module";
diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts
index 2acd60765f..1f1ae6a8d2 100644
--- a/libs/components/src/index.ts
+++ b/libs/components/src/index.ts
@@ -4,6 +4,7 @@ export * from "./button";
export * from "./callout";
export * from "./form-field";
export * from "./icon";
+export * from "./icon-button";
export * from "./menu";
export * from "./dialog";
export * from "./submit-button";
diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css
index 3801dd2e9e..f76c838585 100644
--- a/libs/components/src/tw-theme.css
+++ b/libs/components/src/tw-theme.css
@@ -1,4 +1,6 @@
:root {
+ --color-transparent-hover: rgb(0 0 0 / 0.03);
+
--color-background: 255 255 255;
--color-background-alt: 251 251 251;
--color-background-alt2: 23 92 219;
@@ -37,6 +39,8 @@
}
.theme_dark {
+ --color-transparent-hover: rgb(255 255 255 / 0.12);
+
--color-background: 31 36 46;
--color-background-alt: 22 28 38;
--color-background-alt2: 47 52 61;
diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js
index 67eb96a6c3..082abe7372 100644
--- a/libs/components/tailwind.config.base.js
+++ b/libs/components/tailwind.config.base.js
@@ -12,7 +12,10 @@ module.exports = {
corePlugins: { preflight: false },
theme: {
colors: {
- transparent: colors.transparent,
+ transparent: {
+ DEFAULT: colors.transparent,
+ hover: "var(--color-transparent-hover)",
+ },
current: colors.current,
black: colors.black,
primary: {