1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-14 23:45:37 +00:00

add new button styles

This commit is contained in:
Bryan Cunningham
2026-01-08 15:11:38 -05:00
parent 0396b4e054
commit ca7697f8d4
5 changed files with 231 additions and 59 deletions

View File

@@ -19,50 +19,133 @@ import { ariaDisableElement } from "../utils";
const focusRing = [
"focus-visible:tw-ring-2",
"focus-visible:tw-ring-offset-2",
"focus-visible:tw-ring-primary-600",
"focus-visible:tw-ring-offset-1",
"focus-visible:tw-ring-border-focus",
"focus-visible:tw-z-10",
];
const buttonSizeStyles: Record<ButtonSize, string[]> = {
small: ["tw-py-1", "tw-px-3", "tw-text-sm"],
default: ["tw-py-1.5", "tw-px-3"],
const getButtonSizeStyles = (size: ButtonSize): string[] => {
const buttonSizeStyles: Record<ButtonSize, string[]> = {
small: ["tw-py-1", "tw-px-3", "tw-text-xs"],
default: ["tw-py-2.5", "tw-px-4", "tw-text-sm/5"],
large: ["tw-py-3", "tw-px-4", "tw-text-base/6"],
};
return buttonSizeStyles[size];
};
const buttonStyles: Record<ButtonType, string[]> = {
primary: [
"tw-border-primary-600",
"tw-bg-primary-600",
"!tw-text-contrast",
"hover:tw-bg-primary-700",
"hover:tw-border-primary-700",
const getButtonStyles = (buttonType: ButtonType, size: ButtonSize): string[] => {
const normalizedType = buttonType.toLowerCase();
const buttonStyles: Record<ButtonType, string[]> = {
primary: [
"tw-border-border-brand",
"tw-bg-bg-brand",
"hover:tw-bg-bg-brand-strong",
"hover:tw-border-bg-brand-strong",
],
primaryOutline: [
"tw-border-border-brand",
"tw-text-fg-brand",
"hover:tw-border-bg-brand-strong",
"hover:tw-text-fg-brand-strong",
],
primaryGhost: ["tw-text-fg-heading", "hover:tw-text-fg-brand"],
secondary: [
"tw-bg-bg-secondary",
"tw-border-border-base",
"tw-text-fg-heading",
"hover:tw-bg-bg-quaternary",
"hover:tw-text-fg-brand",
],
subtle: [
"tw-border-border-contrast",
"tw-bg-bg-contrast",
"hover:tw-bg-bg-contrast-strong",
"hover:tw-border-border-contrast-strong",
],
subtleOutline: [
"tw-border-border-contrast",
"tw-text-fg-heading",
"hover:tw-border-border-contrast-strong",
"hover:tw-text-fg-heading",
],
subtleGhost: ["tw-text-fg-heading", "hover:tw-text-fg-heading"],
danger: [
"tw-bg-bg-danger",
"tw-border-border-danger",
"hover:tw-bg-bg-danger-strong",
"hover:tw-border-border-danger-strong",
"hover:tw-text-fg-contrast",
],
dangerOutline: [
"tw-border-border-danger",
"tw-text-fg-danger",
"hover:tw-border-bg-danger-strong",
"hover:!tw-text-fg-danger-strong",
],
dangerGhost: ["tw-text-fg-danger", "hover:tw-text-fg-danger"],
warning: [
"tw-bg-bg-warning",
"tw-border-border-warning",
"hover:tw-bg-bg-warning-strong",
"hover:tw-border-border-warning-strong",
],
warningOutline: [
"tw-border-border-warning",
"tw-text-fg-warning",
"hover:tw-border-border-warning-strong",
"hover:!tw-text-fg-warning-strong",
],
warningGhost: ["tw-text-fg-warning", "hover:tw-text-fg-warning"],
success: [
"tw-bg-bg-success",
"tw-border-border-success",
"hover:tw-bg-bg-success-strong",
"hover:tw-border-border-success-strong",
],
successOutline: [
"tw-border-border-success",
"tw-text-fg-success",
"hover:tw-border-border-success-strong",
"hover:tw-text-fg-success-strong",
],
successGhost: ["tw-text-fg-success", "hover:tw-text-fg-success"],
unstyled: [],
};
const baseStyles = [
"tw-font-medium",
"tw-tracking-wide",
"tw-rounded-xl",
"tw-transition",
"tw-border",
"tw-border-solid",
"tw-text-center",
"tw-no-underline",
"hover:tw-no-underline",
"focus:tw-outline-none",
...focusRing,
],
secondary: [
"tw-bg-transparent",
"tw-border-primary-600",
"!tw-text-primary-600",
"hover:tw-bg-hover-default",
...focusRing,
],
danger: [
"tw-bg-transparent",
"tw-border-danger-600",
"!tw-text-danger",
"hover:tw-bg-danger-600",
"hover:tw-border-danger-600",
"hover:!tw-text-contrast",
...focusRing,
],
dangerPrimary: [
"tw-border-danger-600",
"tw-bg-danger-600",
"!tw-text-contrast",
"hover:tw-bg-danger-700",
"hover:tw-border-danger-700",
...focusRing,
],
unstyled: [],
];
const isOutline = normalizedType.includes("outline");
const isGhost = normalizedType.includes("ghost");
const isSecondary = normalizedType === "secondary";
const isSolid = !isOutline && !isGhost;
if (isOutline || isGhost) {
baseStyles.push("tw-bg-transparent", "hover:tw-bg-bg-hover");
}
if (isSolid && !isSecondary) {
baseStyles.push("tw-text-fg-contrast", "hover:tw-text-fg-contrast");
}
if (isGhost) {
baseStyles.push("tw-border-transparent", "tw-bg-clip-padding", "hover:tw-border-bg-hover");
}
return [...baseStyles, ...buttonStyles[buttonType], ...getButtonSizeStyles(size)];
};
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
@@ -76,19 +159,8 @@ const buttonStyles: Record<ButtonType, string[]> = {
})
export class ButtonComponent implements ButtonLikeAbstraction {
@HostBinding("class") get classList() {
return [
"tw-font-medium",
"tw-rounded-full",
"tw-transition",
"tw-border-2",
"tw-border-solid",
"tw-text-center",
"tw-no-underline",
"hover:tw-no-underline",
"focus:tw-outline-none",
]
return []
.concat(this.block() ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
.concat(buttonStyles[this.buttonType() ?? "secondary"])
.concat(
this.showDisabledStyles() || this.disabled()
? [
@@ -103,7 +175,7 @@ export class ButtonComponent implements ButtonLikeAbstraction {
]
: [],
)
.concat(buttonSizeStyles[this.size() || "default"]);
.concat(getButtonStyles(this.buttonType() || "secondary", this.size() || "default"));
}
protected readonly disabledAttr = computed(() => {

View File

@@ -29,7 +29,7 @@ export default {
},
argTypes: {
size: {
options: ["small", "default"],
options: ["small", "default", "large"],
control: { type: "radio" },
},
},
@@ -62,10 +62,38 @@ export const Primary: Story = {
},
};
export const DangerPrimary: Story = {
export const PrimaryOutline: Story = {
...Default,
args: {
buttonType: "dangerPrimary",
buttonType: "primaryOutline",
},
};
export const PrimaryGhost: Story = {
...Default,
args: {
buttonType: "primaryGhost",
},
};
export const Subtle: Story = {
...Default,
args: {
buttonType: "subtle",
},
};
export const SubtleOutline: Story = {
...Default,
args: {
buttonType: "subtleOutline",
},
};
export const SubtleGhost: Story = {
...Default,
args: {
buttonType: "subtleGhost",
},
};
@@ -76,6 +104,62 @@ export const Danger: Story = {
},
};
export const DangerOutline: Story = {
...Default,
args: {
buttonType: "dangerOutline",
},
};
export const DangerGhost: Story = {
...Default,
args: {
buttonType: "dangerGhost",
},
};
export const Warning: Story = {
...Default,
args: {
buttonType: "warning",
},
};
export const WarningOutline: Story = {
...Default,
args: {
buttonType: "warningOutline",
},
};
export const WarningGhost: Story = {
...Default,
args: {
buttonType: "warningGhost",
},
};
export const Success: Story = {
...Default,
args: {
buttonType: "success",
},
};
export const SuccessOutline: Story = {
...Default,
args: {
buttonType: "successOutline",
},
};
export const SuccessGhost: Story = {
...Default,
args: {
buttonType: "successGhost",
},
};
export const Small: Story = {
render: (args) => ({
props: args,
@@ -84,7 +168,6 @@ export const Small: Story = {
<button type="button" bitButton [disabled]="disabled" [loading]="loading" buttonType="primary" [size]="size" [block]="block">Primary small</button>
<button type="button" bitButton [disabled]="disabled" [loading]="loading" buttonType="secondary" [size]="size" [block]="block">Secondary small</button>
<button type="button" bitButton [disabled]="disabled" [loading]="loading" buttonType="danger" [size]="size" [block]="block">Danger small</button>
<button type="button" bitButton [disabled]="disabled" [loading]="loading" buttonType="dangerPrimary" [size]="size" [block]="block">Danger Primary small</button>
</div>
`,
}),

View File

@@ -1,8 +1,25 @@
import { ModelSignal } from "@angular/core";
export type ButtonType = "primary" | "secondary" | "danger" | "dangerPrimary" | "unstyled";
export type ButtonType =
| "primary"
| "primaryOutline"
| "primaryGhost"
| "secondary"
| "subtle"
| "subtleOutline"
| "subtleGhost"
| "danger"
| "dangerOutline"
| "dangerGhost"
| "warning"
| "warningOutline"
| "warningGhost"
| "success"
| "successOutline"
| "successGhost"
| "unstyled";
export type ButtonSize = "default" | "small";
export type ButtonSize = "default" | "small" | "large";
export abstract class ButtonLikeAbstraction {
abstract loading: ModelSignal<boolean>;

View File

@@ -330,7 +330,7 @@
/* Brand Border */
--color-border-brand-soft: var(--color-brand-200);
--color-border-brand: var(--color-brand-700);
--color-border-brand-strong: var(--color-brand-900);
--color-border-brand-strong: var(--color-brand-800);
/* Status Border */
--color-border-success-soft: var(--color-green-200);

View File

@@ -17,7 +17,7 @@
</ng-container>
<ng-container bitDialogFooter>
<button bitButton type="button" buttonType="dangerPrimary" (click)="confirmLeave()">
<button bitButton type="button" buttonType="danger" (click)="confirmLeave()">
{{ "leaveConfirmationDialogConfirmButton" | i18n: organizationName }}
</button>