mirror of
https://github.com/bitwarden/clients.git
synced 2025-12-05 19:17:06 -06:00
[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:
@@ -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>
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
14
libs/components/src/card/base-card/base-card.component.ts
Normal file
14
libs/components/src/card/base-card/base-card.component.ts
Normal 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 {}
|
||||
@@ -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 {}
|
||||
23
libs/components/src/card/base-card/base-card.mdx
Normal file
23
libs/components/src/card/base-card/base-card.mdx
Normal 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";
|
||||
```
|
||||
41
libs/components/src/card/base-card/base-card.stories.ts
Normal file
41
libs/components/src/card/base-card/base-card.stories.ts
Normal 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><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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
2
libs/components/src/card/base-card/index.ts
Normal file
2
libs/components/src/card/base-card/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./base-card.component";
|
||||
export * from "./base-card.directive";
|
||||
7
libs/components/src/card/card-content.component.ts
Normal file
7
libs/components/src/card/card-content.component.ts
Normal 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 {}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from "./base-card";
|
||||
export * from "./card.component";
|
||||
export * from "./card-content.component";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user