[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
This commit is contained in:
Bryan Cunningham
2025-10-27 11:14:42 -04:00
committed by GitHub
parent af6e19335d
commit f452f39f3c
21 changed files with 184 additions and 57 deletions

View File

@@ -1,8 +1,8 @@
<a
class="tw-block tw-h-full tw-max-w-72 tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 !tw-text-main tw-transition-all hover:tw-scale-105 hover:tw-no-underline focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2"
class="tw-block tw-h-full tw-max-w-72 tw-rounded-xl !tw-text-main tw-transition-all hover:tw-scale-105 hover:tw-no-underline focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2"
[routerLink]="route"
>
<div class="tw-relative">
<bit-base-card class="tw-relative tw-overflow-hidden tw-h-full">
<div
class="tw-flex tw-h-28 tw-bg-background-alt2 tw-text-center tw-text-primary-300"
[ngClass]="{ 'tw-grayscale': disabled }"
@@ -11,10 +11,10 @@
<bit-icon [icon]="icon" aria-hidden="true"></bit-icon>
</div>
</div>
<div class="tw-p-5" [ngClass]="{ 'tw-grayscale': disabled }">
<bit-card-content [ngClass]="{ 'tw-grayscale': disabled }">
<h3 class="tw-mb-4 tw-text-xl tw-font-bold">{{ title }}</h3>
<p class="tw-mb-0">{{ description }}</p>
</div>
</bit-card-content>
<span
bitBadge
[variant]="requiresPremium ? 'success' : 'primary'"
@@ -24,5 +24,5 @@
<ng-container *ngIf="requiresPremium">{{ "premium" | i18n }}</ng-container>
<ng-container *ngIf="!requiresPremium">{{ "upgrade" | i18n }}</ng-container>
</span>
</div>
</bit-base-card>
</a>

View File

@@ -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)],

View File

@@ -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({

View File

@@ -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],
})

View File

@@ -1,5 +1,5 @@
<div
class="tw-block tw-h-full tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-600 tw-relative tw-transition-all hover:tw-scale-105 focus-within:tw-outline-none focus-within:tw-ring focus-within:tw-ring-primary-700 focus-within:tw-ring-offset-2"
<bit-base-card
class="tw-block tw-h-full tw-overflow-hidden tw-relative tw-transition-all hover:tw-scale-105 focus-within:tw-outline-none focus-within:tw-ring focus-within:tw-ring-primary-700 focus-within:tw-ring-offset-2"
>
<div class="tw-flex tw-bg-secondary-100 tw-items-center tw-justify-end tw-pt-4 tw-pr-4">
<i class="bwi bwi-external-link"></i>
@@ -27,8 +27,8 @@
</a>
}
</div>
<div class="tw-p-5">
<h3 class="tw-text-main tw-text-lg tw-font-semibold">
<bit-card-content>
<h3 class="tw-text-main tw-m-0 tw-text-lg tw-font-semibold">
{{ name }}
@if (showConnectedBadge()) {
<span class="tw-ml-3">
@@ -41,8 +41,9 @@
</span>
}
</h3>
<p class="tw-mb-0 tw-font-semibold">{{ description }}</p>
@if (description) {
<p class="tw-mb-0 tw-mt-2 tw-font-semibold">{{ description }}</p>
}
@if (canSetupConnection) {
<button type="button" class="tw-mt-3" bitButton (click)="setupConnection()">
@if (isUpdateAvailable) {
@@ -58,5 +59,5 @@
{{ "new" | i18n }}
</span>
}
</div>
</div>
</bit-card-content>
</bit-base-card>

View File

@@ -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<void> = new Subject();

View File

@@ -48,11 +48,11 @@
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</div>
} @else {
<div
class="tw-rounded-2xl tw-mb-6 sm:tw-mb-10 tw-mx-auto tw-w-full sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
<bit-base-card
class="!tw-rounded-2xl tw-mb-6 sm:tw-mb-10 tw-mx-auto tw-w-full tw-bg-transparent tw-border-none tw-shadow-none sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-100 sm:tw-shadow sm:tw-p-8"
>
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</div>
</bit-base-card>
}
<ng-content select="[slot=secondary]"></ng-content>
</div>

View File

@@ -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")

View File

@@ -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 `<bit-card>` component should suffice.
*/
@Component({
selector: "bit-base-card",
template: `<ng-content></ng-content>`,
hostDirectives: [BaseCardDirective],
})
export class BaseCardComponent {}

View File

@@ -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 {}

View File

@@ -0,0 +1,23 @@
import { Meta, Primary, Controls, Canvas, Title, Description } from "@storybook/addon-docs";
import * as stories from "./base-card.stories";
<Meta of={stories} />
```ts
import { BaseCardComponent } from "@bitwarden/components";
```
<Title />
<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";
```

View File

@@ -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>&lt;bit-base-card&gt;</code> component is a container that applies our standard border and box-shadow. In most cases, <code>&lt;bit-card&gt;</code> should be used for consistency
</p>
<p bitTypography="body1" class="!tw-mb-0">
<code>&lt;bit-base-card&gt;</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>
`,
}),
};

View File

@@ -0,0 +1,2 @@
export * from "./base-card.component";
export * from "./base-card.directive";

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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>
`,
}),
};

View File

@@ -1 +1,3 @@
export * from "./base-card";
export * from "./card.component";
export * from "./card-content.component";

View File

@@ -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>

View File

@@ -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 {
/**

View File

@@ -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>

View File

@@ -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>();