From ca7697f8d4478c03ebd70592474d93a07c6c9f4a Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Thu, 8 Jan 2026 15:11:38 -0500 Subject: [PATCH] add new button styles --- .../components/src/button/button.component.ts | 174 +++++++++++++----- libs/components/src/button/button.stories.ts | 91 ++++++++- .../src/shared/button-like.abstraction.ts | 21 ++- libs/components/src/tw-theme.css | 2 +- .../leave-confirmation-dialog.component.html | 2 +- 5 files changed, 231 insertions(+), 59 deletions(-) diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 7cae8fe974d..3f65420b7fd 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -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 = { - 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 = { + 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 = { - 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 = { + 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 = { }) 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(() => { diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 24c263f240a..b31f2fbd355 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -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 = { - `, }), diff --git a/libs/components/src/shared/button-like.abstraction.ts b/libs/components/src/shared/button-like.abstraction.ts index 45a661b6ecb..ede084a2d80 100644 --- a/libs/components/src/shared/button-like.abstraction.ts +++ b/libs/components/src/shared/button-like.abstraction.ts @@ -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; diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 757859985d6..004e9d4c6fe 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -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); diff --git a/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.html b/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.html index 6d1045e1a86..91fa74d030f 100644 --- a/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.html +++ b/libs/vault/src/components/vault-items-transfer/leave-confirmation-dialog.component.html @@ -17,7 +17,7 @@ -