From f452f39f3c1b6204876602e4212788241cea0c3d Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Mon, 27 Oct 2025 11:14:42 -0400 Subject: [PATCH] [CL-847] Card consolidation (#16952) * created shared card directive * WIP * use base card in anon layout * use bit-card for pricing card component * add base card to integration cards * add base card to reports cards * add base card to integration card * use card content on report card * use base card directive on base component * update dirt card to use bit-card * run prettier. fix whitespace * add missing imports to report list stories * add base card story and docs --- .../report-card/report-card.component.html | 10 ++--- .../shared/report-card/report-card.stories.ts | 17 +++++++- .../shared/report-list/report-list.stories.ts | 17 +++++++- .../reports/shared/reports-shared.module.ts | 4 +- .../integration-card.component.html | 17 ++++---- .../integration-card.component.ts | 10 ++++- .../anon-layout/anon-layout.component.html | 6 +-- .../src/anon-layout/anon-layout.component.ts | 10 ++++- .../src/card/base-card/base-card.component.ts | 14 +++++++ .../src/card/base-card/base-card.directive.ts | 9 ++++ .../src/card/base-card/base-card.mdx | 23 +++++++++++ .../src/card/base-card/base-card.stories.ts | 41 +++++++++++++++++++ libs/components/src/card/base-card/index.ts | 2 + .../src/card/card-content.component.ts | 7 ++++ libs/components/src/card/card.component.ts | 6 ++- libs/components/src/card/card.stories.ts | 15 +------ libs/components/src/card/index.ts | 2 + libs/dirt/card/src/card.component.html | 14 ++++--- libs/dirt/card/src/card.component.ts | 8 +--- .../pricing-card/pricing-card.component.html | 6 +-- .../pricing-card/pricing-card.component.ts | 3 +- 21 files changed, 184 insertions(+), 57 deletions(-) create mode 100644 libs/components/src/card/base-card/base-card.component.ts create mode 100644 libs/components/src/card/base-card/base-card.directive.ts create mode 100644 libs/components/src/card/base-card/base-card.mdx create mode 100644 libs/components/src/card/base-card/base-card.stories.ts create mode 100644 libs/components/src/card/base-card/index.ts create mode 100644 libs/components/src/card/card-content.component.ts diff --git a/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.html b/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.html index 8db0db3b5e..dab928e6ec 100644 --- a/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.html +++ b/apps/web/src/app/dirt/reports/shared/report-card/report-card.component.html @@ -1,8 +1,8 @@ -
+
-
+

{{ title }}

{{ description }}

-
+ {{ "premium" | i18n }} {{ "upgrade" | i18n }} - +
diff --git a/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts b/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts index 76951bf945..50798fea6e 100644 --- a/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts +++ b/apps/web/src/app/dirt/reports/shared/report-card/report-card.stories.ts @@ -4,7 +4,12 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BadgeModule, IconModule } from "@bitwarden/components"; +import { + BadgeModule, + BaseCardComponent, + IconModule, + CardContentComponent, +} from "@bitwarden/components"; import { PreloadedEnglishI18nModule } from "../../../../core/tests"; import { ReportVariant } from "../models/report-variant"; @@ -16,7 +21,15 @@ export default { component: ReportCardComponent, decorators: [ moduleMetadata({ - imports: [JslibModule, BadgeModule, IconModule, RouterTestingModule, PremiumBadgeComponent], + imports: [ + JslibModule, + BadgeModule, + CardContentComponent, + IconModule, + RouterTestingModule, + PremiumBadgeComponent, + BaseCardComponent, + ], }), applicationConfig({ providers: [importProvidersFrom(PreloadedEnglishI18nModule)], diff --git a/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts b/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts index 22c7e851be..5a89eeff80 100644 --- a/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts +++ b/apps/web/src/app/dirt/reports/shared/report-list/report-list.stories.ts @@ -4,7 +4,12 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BadgeModule, IconModule } from "@bitwarden/components"; +import { + BadgeModule, + BaseCardComponent, + CardContentComponent, + IconModule, +} from "@bitwarden/components"; import { PreloadedEnglishI18nModule } from "../../../../core/tests"; import { reports } from "../../reports"; @@ -18,7 +23,15 @@ export default { component: ReportListComponent, decorators: [ moduleMetadata({ - imports: [JslibModule, BadgeModule, RouterTestingModule, IconModule, PremiumBadgeComponent], + imports: [ + JslibModule, + BadgeModule, + RouterTestingModule, + IconModule, + PremiumBadgeComponent, + CardContentComponent, + BaseCardComponent, + ], declarations: [ReportCardComponent], }), applicationConfig({ diff --git a/apps/web/src/app/dirt/reports/shared/reports-shared.module.ts b/apps/web/src/app/dirt/reports/shared/reports-shared.module.ts index cad5d06d79..59e59a6a50 100644 --- a/apps/web/src/app/dirt/reports/shared/reports-shared.module.ts +++ b/apps/web/src/app/dirt/reports/shared/reports-shared.module.ts @@ -1,13 +1,15 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; +import { BaseCardComponent, CardContentComponent } from "@bitwarden/components"; + import { SharedModule } from "../../../shared/shared.module"; import { ReportCardComponent } from "./report-card/report-card.component"; import { ReportListComponent } from "./report-list/report-list.component"; @NgModule({ - imports: [CommonModule, SharedModule], + imports: [CommonModule, SharedModule, BaseCardComponent, CardContentComponent], declarations: [ReportCardComponent, ReportListComponent], exports: [ReportCardComponent, ReportListComponent], }) diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html index 423b013038..19a12755ca 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.html @@ -1,5 +1,5 @@ -
@@ -27,8 +27,8 @@ }
-
-

+ +

{{ name }} @if (showConnectedBadge()) { @@ -41,8 +41,9 @@ }

-

{{ description }}

- + @if (description) { +

{{ description }}

+ } @if (canSetupConnection) {

-
+ + diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts index f1b0f982d5..e6d4aff05f 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts @@ -20,7 +20,13 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; +import { + BaseCardComponent, + CardContentComponent, + DialogRef, + DialogService, + ToastService, +} from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { @@ -37,7 +43,7 @@ import { @Component({ selector: "app-integration-card", templateUrl: "./integration-card.component.html", - imports: [SharedModule], + imports: [SharedModule, BaseCardComponent, CardContentComponent], }) export class IntegrationCardComponent implements AfterViewInit, OnDestroy { private destroyed$: Subject = new Subject(); diff --git a/libs/components/src/anon-layout/anon-layout.component.html b/libs/components/src/anon-layout/anon-layout.component.html index f88bdd3f92..15f7d10754 100644 --- a/libs/components/src/anon-layout/anon-layout.component.html +++ b/libs/components/src/anon-layout/anon-layout.component.html @@ -48,11 +48,11 @@ } @else { -
-
+ } diff --git a/libs/components/src/anon-layout/anon-layout.component.ts b/libs/components/src/anon-layout/anon-layout.component.ts index 596a54f882..e6572a0c3c 100644 --- a/libs/components/src/anon-layout/anon-layout.component.ts +++ b/libs/components/src/anon-layout/anon-layout.component.ts @@ -21,6 +21,7 @@ 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 { BaseCardComponent } from "../card"; import { IconModule } from "../icon"; import { SharedModule } from "../shared"; import { TypographyModule } from "../typography"; @@ -32,7 +33,14 @@ export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl"; @Component({ selector: "auth-anon-layout", templateUrl: "./anon-layout.component.html", - imports: [IconModule, CommonModule, TypographyModule, SharedModule, RouterModule], + imports: [ + IconModule, + CommonModule, + TypographyModule, + SharedModule, + RouterModule, + BaseCardComponent, + ], }) export class AnonLayoutComponent implements OnInit, OnChanges { @HostBinding("class") diff --git a/libs/components/src/card/base-card/base-card.component.ts b/libs/components/src/card/base-card/base-card.component.ts new file mode 100644 index 0000000000..44f82a32c4 --- /dev/null +++ b/libs/components/src/card/base-card/base-card.component.ts @@ -0,0 +1,14 @@ +import { Component } from "@angular/core"; + +import { BaseCardDirective } from "./base-card.directive"; + +/** + * The base card component is a container that applies our standard card border and box-shadow. + * In most cases using our `` component should suffice. + */ +@Component({ + selector: "bit-base-card", + template: ``, + hostDirectives: [BaseCardDirective], +}) +export class BaseCardComponent {} diff --git a/libs/components/src/card/base-card/base-card.directive.ts b/libs/components/src/card/base-card/base-card.directive.ts new file mode 100644 index 0000000000..7c6ec2b3b2 --- /dev/null +++ b/libs/components/src/card/base-card/base-card.directive.ts @@ -0,0 +1,9 @@ +import { Directive } from "@angular/core"; + +@Directive({ + host: { + class: + "tw-box-border tw-block tw-bg-background tw-text-main tw-border tw-border-solid tw-border-secondary-100 tw-shadow tw-rounded-xl", + }, +}) +export class BaseCardDirective {} diff --git a/libs/components/src/card/base-card/base-card.mdx b/libs/components/src/card/base-card/base-card.mdx new file mode 100644 index 0000000000..df32646290 --- /dev/null +++ b/libs/components/src/card/base-card/base-card.mdx @@ -0,0 +1,23 @@ +import { Meta, Primary, Controls, Canvas, Title, Description } from "@storybook/addon-docs"; + +import * as stories from "./base-card.stories"; + + + +```ts +import { BaseCardComponent } from "@bitwarden/components"; +``` + + +<Description /> + +<Canvas of={stories.Default} /> + +## BaseCardDirective + +There is also a `BaseCardDirective` available for use as a hostDirective if need be. But, most +likely using `<bit-base-card>` in your template will do. + +```ts +import { BaseCardDirective } from "@bitwarden/components"; +``` diff --git a/libs/components/src/card/base-card/base-card.stories.ts b/libs/components/src/card/base-card/base-card.stories.ts new file mode 100644 index 0000000000..bae07dd146 --- /dev/null +++ b/libs/components/src/card/base-card/base-card.stories.ts @@ -0,0 +1,41 @@ +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; + +import { AnchorLinkDirective } from "../../link"; +import { TypographyModule } from "../../typography"; + +import { BaseCardComponent } from "./base-card.component"; + +export default { + title: "Component Library/Cards/BaseCard", + component: BaseCardComponent, + decorators: [ + moduleMetadata({ + imports: [AnchorLinkDirective, TypographyModule], + }), + ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28355&t=b5tDKylm5sWm2yKo-4", + }, + }, +} as Meta; + +type Story = StoryObj<BaseCardComponent>; + +/** Cards are presentational containers. */ +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + <bit-base-card> + <p bitTypography="body1" class="!tw-mb-0"> + The <code><bit-base-card></code> component is a container that applies our standard border and box-shadow. In most cases, <code><bit-card></code> should be used for consistency + </p> + <p bitTypography="body1" class="!tw-mb-0"> + <code><bit-base-card></code> is used in the <a bitLink href="/?path=/story/web-reports-card--enabled">ReportCardComponent</a> and <strong>IntegrationsCardComponent</strong> since they have custom padding requirements + </p> + </bit-base-card> + `, + }), +}; diff --git a/libs/components/src/card/base-card/index.ts b/libs/components/src/card/base-card/index.ts new file mode 100644 index 0000000000..186f2e68f2 --- /dev/null +++ b/libs/components/src/card/base-card/index.ts @@ -0,0 +1,2 @@ +export * from "./base-card.component"; +export * from "./base-card.directive"; diff --git a/libs/components/src/card/card-content.component.ts b/libs/components/src/card/card-content.component.ts new file mode 100644 index 0000000000..60be20e78f --- /dev/null +++ b/libs/components/src/card/card-content.component.ts @@ -0,0 +1,7 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "bit-card-content", + template: `<div class="tw-p-4 [@media(min-width:650px)]:tw-p-6"><ng-content></ng-content></div>`, +}) +export class CardContentComponent {} diff --git a/libs/components/src/card/card.component.ts b/libs/components/src/card/card.component.ts index d7e36d1ea9..9cca973f00 100644 --- a/libs/components/src/card/card.component.ts +++ b/libs/components/src/card/card.component.ts @@ -1,12 +1,14 @@ import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { BaseCardDirective } from "./base-card/base-card.directive"; + @Component({ selector: "bit-card", template: `<ng-content></ng-content>`, changeDetection: ChangeDetectionStrategy.OnPush, host: { - class: - "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg [&:not(bit-layout_*)]:tw-border-b-shadow tw-py-4 bit-compact:tw-py-3 tw-px-3 bit-compact:tw-px-2", + class: "tw-p-4 [@media(min-width:650px)]:tw-p-6", }, + hostDirectives: [BaseCardDirective], }) export class CardComponent {} diff --git a/libs/components/src/card/card.stories.ts b/libs/components/src/card/card.stories.ts index 411cc8e83c..77faceb8eb 100644 --- a/libs/components/src/card/card.stories.ts +++ b/libs/components/src/card/card.stories.ts @@ -11,7 +11,7 @@ import { I18nMockService } from "../utils/i18n-mock.service"; import { CardComponent } from "./card.component"; export default { - title: "Component Library/Card", + title: "Component Library/Cards/Card", component: CardComponent, decorators: [ moduleMetadata({ @@ -84,16 +84,3 @@ export const WithinSections: Story = { `, }), }; - -export const WithoutBorderRadius: Story = { - render: (args) => ({ - props: args, - template: /*html*/ ` - <bit-layout> - <bit-card> - <p bitTypography="body1" class="!tw-mb-0">Cards used in <code class="tw-text-danger-700">bit-layout</code> will not have a border radius</p> - </bit-card> - </bit-layout> - `, - }), -}; diff --git a/libs/components/src/card/index.ts b/libs/components/src/card/index.ts index 8151bac4c8..1027f9b1fe 100644 --- a/libs/components/src/card/index.ts +++ b/libs/components/src/card/index.ts @@ -1 +1,3 @@ +export * from "./base-card"; export * from "./card.component"; +export * from "./card-content.component"; diff --git a/libs/dirt/card/src/card.component.html b/libs/dirt/card/src/card.component.html index 3fd9372087..8688cd8fd2 100644 --- a/libs/dirt/card/src/card.component.html +++ b/libs/dirt/card/src/card.component.html @@ -1,7 +1,9 @@ -<div class="tw-flex-col"> - <span bitTypography="body2" class="tw-flex tw-text-muted">{{ title }}</span> - <div class="tw-flex tw-items-baseline tw-gap-2"> - <span bitTypography="h1">{{ value }}</span> - <span bitTypography="body2">{{ "cardMetrics" | i18n: maxValue }}</span> +<bit-card> + <div class="tw-flex tw-flex-col tw-gap-1.5"> + <span bitTypography="body2" class="tw-flex tw-text-muted">{{ title }}</span> + <div class="tw-flex tw-items-baseline tw-gap-2"> + <span bitTypography="h1" class="!tw-mb-0">{{ value }}</span> + <span bitTypography="body2">{{ "cardMetrics" | i18n: maxValue }}</span> + </div> </div> -</div> +</bit-card> diff --git a/libs/dirt/card/src/card.component.ts b/libs/dirt/card/src/card.component.ts index b9f2e7aa72..089115fc2b 100644 --- a/libs/dirt/card/src/card.component.ts +++ b/libs/dirt/card/src/card.component.ts @@ -4,18 +4,14 @@ import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { TypographyModule } from "@bitwarden/components"; +import { TypographyModule, CardComponent as BitCardComponent } from "@bitwarden/components"; // 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: "dirt-card", templateUrl: "./card.component.html", - imports: [CommonModule, TypographyModule, JslibModule], - host: { - class: - "tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-rounded-lg tw-p-6", - }, + imports: [CommonModule, TypographyModule, JslibModule, BitCardComponent], }) export class CardComponent { /** diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.html b/libs/pricing/src/components/pricing-card/pricing-card.component.html index 8eae7088ac..bc0ca68c5c 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.html +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.html @@ -1,6 +1,4 @@ -<div - class="tw-box-border tw-bg-background tw-text-main tw-border tw-border-secondary-100 tw-rounded-3xl tw-p-8 tw-shadow-sm tw-size-full tw-flex tw-flex-col" -> +<bit-card class="tw-size-full tw-flex tw-flex-col"> <!-- Title Section with Active Badge --> <div class="tw-flex tw-items-center tw-justify-between tw-mb-2"> <ng-content select="[slot=title]"></ng-content> @@ -82,4 +80,4 @@ } } </div> -</div> +</bit-card> diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.ts index a8fed031ad..f268c65433 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.ts +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.ts @@ -6,6 +6,7 @@ import { BadgeVariant, ButtonModule, ButtonType, + CardComponent, IconModule, TypographyModule, } from "@bitwarden/components"; @@ -20,7 +21,7 @@ import { @Component({ selector: "billing-pricing-card", templateUrl: "./pricing-card.component.html", - imports: [BadgeModule, ButtonModule, IconModule, TypographyModule, CurrencyPipe], + imports: [BadgeModule, ButtonModule, IconModule, TypographyModule, CurrencyPipe, CardComponent], }) export class PricingCardComponent { readonly tagline = input.required<string>();