From 1578886a5f6824c569e04bf45f294fb448f482ca Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Wed, 21 Jan 2026 10:45:49 -0500 Subject: [PATCH] [CL-984] link style updates (#18360) * WIP * add new link styles * update link stories * skip default screenshot as variations are covered in other stories * updated docs and story background * make default the default linkType value * remove references to primary link type in CL * use better bg colors in stories * remove duplicate linkType * update aria-disabled text to use new palette * add back primary link type to story * fix capitolization * add backticks to variant names in docs * remove important from link styles * fix generic selector to find correct button * fix capitolization * mark variants as deprecated in docs * fix link hover text colors --- ...fill-confirmation-dialog.component.spec.ts | 16 +- .../breadcrumbs/breadcrumbs.component.html | 7 +- libs/components/src/link/link.directive.ts | 47 ++++-- libs/components/src/link/link.mdx | 9 +- libs/components/src/link/link.stories.ts | 142 ++++++++++++++++-- .../components/kitchen-sink-form.component.ts | 1 - .../components/kitchen-sink-main.component.ts | 3 +- 7 files changed, 178 insertions(+), 47 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts index a28b8730109..93cc2cf248a 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts @@ -91,10 +91,18 @@ describe("AutofillConfirmationDialogComponent", () => { jest.resetAllMocks(); }); - const findShowAll = (inFx?: ComponentFixture) => - (inFx || fixture).nativeElement.querySelector( - "button.tw-text-sm.tw-font-medium.tw-cursor-pointer", - ) as HTMLButtonElement | null; + const findShowAll = (inFx?: ComponentFixture) => { + // Find the button by its text content (showAll or showLess) + const buttons = Array.from( + (inFx || fixture).nativeElement.querySelectorAll("button"), + ) as HTMLButtonElement[]; + return ( + buttons.find((btn) => { + const text = btn.textContent?.trim() || ""; + return text === "showAll" || text === "showLess"; + }) || null + ); + }; it("normalizes currentUrl and savedUrls via Utils.getHostname", () => { expect(Utils.getHostname).toHaveBeenCalledTimes(1 + (params.savedUrls?.length ?? 0)); diff --git a/libs/components/src/breadcrumbs/breadcrumbs.component.html b/libs/components/src/breadcrumbs/breadcrumbs.component.html index ee5ad79c739..d666c641572 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.component.html +++ b/libs/components/src/breadcrumbs/breadcrumbs.component.html @@ -2,7 +2,6 @@ @if (breadcrumb.route(); as route) { @@ -42,7 +40,6 @@ @if (breadcrumb.route(); as route) { } @else { - } @@ -61,7 +58,6 @@ @if (breadcrumb.route(); as route) { diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.directive.ts index e6de8ac8402..62f0d8b878f 100644 --- a/libs/components/src/link/link.directive.ts +++ b/libs/components/src/link/link.directive.ts @@ -3,21 +3,34 @@ import { input, HostBinding, Directive, inject, ElementRef, booleanAttribute } f import { AriaDisableDirective } from "../a11y"; import { ariaDisableElement } from "../utils"; -export type LinkType = "primary" | "secondary" | "contrast" | "light"; +export const LinkTypes = [ + "primary", + "secondary", + "contrast", + "light", + "default", + "subtle", + "success", + "warning", + "danger", +] as const; + +export type LinkType = (typeof LinkTypes)[number]; const linkStyles: Record = { - primary: [ - "!tw-text-primary-600", - "hover:!tw-text-primary-700", - "focus-visible:before:tw-ring-primary-600", - ], - secondary: ["!tw-text-main", "hover:!tw-text-main", "focus-visible:before:tw-ring-primary-600"], + primary: ["tw-text-fg-brand", "hover:tw-text-fg-brand-strong"], + default: ["tw-text-fg-brand", "hover:tw-text-fg-brand-strong"], + secondary: ["tw-text-fg-heading", "hover:tw-text-fg-heading"], + light: ["tw-text-fg-white", "hover:tw-text-fg-white", "focus-visible:before:tw-ring-fg-contrast"], + subtle: ["!tw-text-fg-heading", "hover:tw-text-fg-heading"], + success: ["tw-text-fg-success", "hover:tw-text-fg-success-strong"], + warning: ["tw-text-fg-warning", "hover:tw-text-fg-warning-strong"], + danger: ["tw-text-fg-danger", "hover:tw-text-fg-danger-strong"], contrast: [ - "!tw-text-contrast", - "hover:!tw-text-contrast", - "focus-visible:before:tw-ring-text-contrast", + "tw-text-fg-contrast", + "hover:tw-text-fg-contrast", + "focus-visible:before:tw-ring-fg-contrast", ], - light: ["!tw-text-alt2", "hover:!tw-text-alt2", "focus-visible:before:tw-ring-text-alt2"], }; const commonStyles = [ @@ -32,16 +45,18 @@ const commonStyles = [ "tw-rounded", "tw-transition", "tw-no-underline", + "tw-cursor-pointer", "hover:tw-underline", "hover:tw-decoration-1", "disabled:tw-no-underline", "disabled:tw-cursor-not-allowed", - "disabled:!tw-text-secondary-300", - "disabled:hover:!tw-text-secondary-300", + "disabled:!tw-text-fg-disabled", + "disabled:hover:!tw-text-fg-disabled", "disabled:hover:tw-no-underline", "focus-visible:tw-outline-none", "focus-visible:tw-underline", "focus-visible:tw-decoration-1", + "focus-visible:before:tw-ring-border-focus", // Workaround for html button tag not being able to be set to `display: inline` // and at the same time not being able to use `tw-ring-offset` because of box-shadow issue. @@ -63,14 +78,14 @@ const commonStyles = [ "focus-visible:tw-z-10", "aria-disabled:tw-no-underline", "aria-disabled:tw-pointer-events-none", - "aria-disabled:!tw-text-secondary-300", - "aria-disabled:hover:!tw-text-secondary-300", + "aria-disabled:!tw-text-fg-disabled", + "aria-disabled:hover:!tw-text-fg-disabled", "aria-disabled:hover:tw-no-underline", ]; @Directive() abstract class LinkDirective { - readonly linkType = input("primary"); + readonly linkType = input("default"); } /** diff --git a/libs/components/src/link/link.mdx b/libs/components/src/link/link.mdx index 072e0dd84d8..4954effb6c0 100644 --- a/libs/components/src/link/link.mdx +++ b/libs/components/src/link/link.mdx @@ -18,10 +18,15 @@ import { LinkModule } from "@bitwarden/components"; You can use one of the following variants by providing it as the `linkType` input: -- `primary` - most common, uses brand color -- `secondary` - matches the main text color +- @deprecated `primary` => use `default` instead +- @deprecated `secondary` => use `subtle` instead +- `default` - most common, uses brand color +- `subtle` - matches the main text color - `contrast` - for high contrast against a dark background (or a light background in dark mode) - `light` - always a light color, even in dark mode +- `warning` - used in association with warning callouts/banners +- `success` - used in association with success callouts/banners +- `danger` - used in association with danger callouts/banners ## Sizes diff --git a/libs/components/src/link/link.stories.ts b/libs/components/src/link/link.stories.ts index ae91c9be108..d27c4f74332 100644 --- a/libs/components/src/link/link.stories.ts +++ b/libs/components/src/link/link.stories.ts @@ -2,7 +2,7 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet"; -import { AnchorLinkDirective, ButtonLinkDirective } from "./link.directive"; +import { AnchorLinkDirective, ButtonLinkDirective, LinkTypes } from "./link.directive"; import { LinkModule } from "./link.module"; export default { @@ -14,7 +14,7 @@ export default { ], argTypes: { linkType: { - options: ["primary", "secondary", "contrast"], + options: LinkTypes.map((type) => type), control: { type: "radio" }, }, }, @@ -30,48 +30,153 @@ type Story = StoryObj; export const Default: Story = { render: (args) => ({ + props: { + linkType: args.linkType, + backgroundClass: + args.linkType === "contrast" + ? "tw-bg-bg-contrast" + : args.linkType === "light" + ? "tw-bg-bg-brand" + : "tw-bg-transparent", + }, template: /*html*/ ` - (args)}>Your text here + `, }), + args: { + linkType: "primary", + }, + parameters: { + chromatic: { disableSnapshot: true }, + }, +}; + +export const AllVariations: Story = { + render: () => ({ + template: /*html*/ ` +
+
+ Primary +
+
+ Secondary +
+
+ Contrast +
+
+ Light +
+
+ Default +
+
+ Subtle +
+
+ Success +
+
+ Warning +
+
+ Danger +
+
+ `, + }), + parameters: { + controls: { + exclude: ["linkType"], + hideNoControlsWarning: true, + }, + }, }; export const InteractionStates: Story = { render: () => ({ template: /*html*/ ` -
+
+ -
+ -
+ - `, }), + parameters: { + controls: { + exclude: ["linkType"], + hideNoControlsWarning: true, + }, + }, }; export const Buttons: Story = { render: (args) => ({ - props: args, + props: { + linkType: args.linkType, + backgroundClass: + args.linkType === "contrast" + ? "tw-bg-bg-contrast" + : args.linkType === "light" + ? "tw-bg-bg-brand" + : "tw-bg-transparent", + }, template: /*html*/ ` -
+
@@ -100,9 +205,17 @@ export const Buttons: Story = { export const Anchors: StoryObj = { render: (args) => ({ - props: args, + props: { + linkType: args.linkType, + backgroundClass: + args.linkType === "contrast" + ? "tw-bg-bg-contrast" + : args.linkType === "light" + ? "tw-bg-bg-brand" + : "tw-bg-transparent", + }, template: /*html*/ ` -
+
@@ -138,18 +251,15 @@ export const Inline: Story = { `, }), - args: { - linkType: "primary", - }, }; -export const Disabled: Story = { +export const Inactive: Story = { render: (args) => ({ props: args, template: /*html*/ ` -
+
`, diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts index 23c95cafb8a..e54064f0c9d 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts @@ -73,7 +73,6 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; A random password