mirror of
https://github.com/bitwarden/browser
synced 2026-02-02 09:43:29 +00:00
wip: add dumb header component to CL; use within app-header
This commit is contained in:
@@ -1,16 +1,9 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { BannerModule } from "@bitwarden/components";
|
||||
|
||||
import { DynamicAvatarComponent } from "../../components/dynamic-avatar.component";
|
||||
import { SharedModule } from "../../shared";
|
||||
import { ProductSwitcherModule } from "../product-switcher/product-switcher.module";
|
||||
|
||||
import { WebHeaderComponent } from "./web-header.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, DynamicAvatarComponent, ProductSwitcherModule, BannerModule],
|
||||
declarations: [WebHeaderComponent],
|
||||
imports: [WebHeaderComponent],
|
||||
exports: [WebHeaderComponent],
|
||||
})
|
||||
export class HeaderModule {}
|
||||
|
||||
@@ -1,110 +1,87 @@
|
||||
<header
|
||||
*ngIf="routeData$ | async as routeData"
|
||||
class="-tw-mt-6 -tw-mx-8 tw-mb-3 tw-flex tw-flex-col tw-py-6 tw-px-8"
|
||||
[ngClass]="{
|
||||
'tw-border-0 tw-border-b tw-border-solid tw-border-secondary-100 tw-bg-background-alt tw-pb-0':
|
||||
tabsContainer.childElementCount !== 0,
|
||||
}"
|
||||
>
|
||||
<div class="tw-flex">
|
||||
<div class="tw-flex tw-min-w-0 tw-flex-1 tw-flex-col tw-gap-2">
|
||||
@let routeData = routeData$ | async;
|
||||
@if (routeData) {
|
||||
<bit-header [title]="title() || (routeData.titleId | i18n)" [icon]="icon()">
|
||||
<ng-container slot="breadcrumbs">
|
||||
<ng-content select="[slot=breadcrumbs]"></ng-content>
|
||||
<h1
|
||||
bitTypography="h1"
|
||||
noMargin
|
||||
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex tw-gap-1"
|
||||
[title]="title || (routeData.titleId | i18n)"
|
||||
</ng-container>
|
||||
|
||||
<ng-content></ng-content>
|
||||
<product-switcher></product-switcher>
|
||||
|
||||
@let account = account$ | async;
|
||||
@if (account) {
|
||||
<button
|
||||
type="button"
|
||||
[bitMenuTriggerFor]="accountMenu"
|
||||
class="tw-border-0 tw-bg-transparent tw-p-0 tw-shrink-0"
|
||||
>
|
||||
<div class="tw-truncate">
|
||||
<i *ngIf="icon" class="bwi {{ icon }}" aria-hidden="true"></i>
|
||||
{{ title || (routeData.titleId | i18n) }}
|
||||
</div>
|
||||
<div><ng-content select="[slot=title-suffix]"></ng-content></div>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="tw-ml-auto tw-flex tw-flex-col tw-gap-4">
|
||||
<div class="tw-flex tw-min-w-max tw-items-center tw-justify-end tw-gap-2">
|
||||
<ng-content></ng-content>
|
||||
<product-switcher></product-switcher>
|
||||
<ng-container *ngIf="account$ | async as account">
|
||||
<button
|
||||
type="button"
|
||||
[bitMenuTriggerFor]="accountMenu"
|
||||
class="tw-border-0 tw-bg-transparent tw-p-0 tw-shrink-0"
|
||||
<dynamic-avatar [id]="account.id" [text]="account | userName"></dynamic-avatar>
|
||||
</button>
|
||||
|
||||
<bit-menu #accountMenu>
|
||||
<div class="tw-flex tw-min-w-52 tw-max-w-72 tw-flex-col">
|
||||
<div
|
||||
class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-muted"
|
||||
appStopProp
|
||||
>
|
||||
<dynamic-avatar [id]="account.id" [text]="account | userName"></dynamic-avatar>
|
||||
</button>
|
||||
|
||||
<bit-menu #accountMenu>
|
||||
<div class="tw-flex tw-min-w-52 tw-max-w-72 tw-flex-col">
|
||||
<div
|
||||
class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-muted"
|
||||
appStopProp
|
||||
>
|
||||
<dynamic-avatar [id]="account.id" [text]="account | userName"></dynamic-avatar>
|
||||
<div class="tw-ml-2 tw-block tw-overflow-hidden tw-whitespace-nowrap">
|
||||
<span>{{ "loggedInAs" | i18n }}</span>
|
||||
<small class="tw-block tw-overflow-hidden tw-whitespace-nowrap">
|
||||
{{ account | userName }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="selfHosted">
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<span class="tw-break-all tw-px-4 tw-py-1 tw-text-left tw-text-muted">
|
||||
{{ hostname }}
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
|
||||
<a bitMenuItem routerLink="/settings/account">
|
||||
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
|
||||
{{ "accountSettings" | i18n }}
|
||||
</a>
|
||||
<a bitMenuItem href="https://bitwarden.com/help/" target="_blank" rel="noreferrer">
|
||||
<i class="bwi bwi-fw bwi-question-circle" aria-hidden="true"></i>
|
||||
{{ "getHelp" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
bitMenuItem
|
||||
href="https://bitwarden.com/download/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-download" aria-hidden="true"></i>
|
||||
{{ "getApps" | i18n }}
|
||||
</a>
|
||||
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
|
||||
<button *ngIf="canLock$ | async" bitMenuItem type="button" (click)="lock()">
|
||||
<i class="bwi bwi-fw bwi-lock" aria-hidden="true"></i>
|
||||
{{ "lockNow" | i18n }}
|
||||
</button>
|
||||
<button bitMenuItem type="button" (click)="logout()">
|
||||
<i class="bwi bwi-fw bwi-sign-out" aria-hidden="true"></i>
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
<div class="tw-ml-2 tw-block tw-overflow-hidden tw-whitespace-nowrap">
|
||||
<span>{{ "loggedInAs" | i18n }}</span>
|
||||
<small class="tw-block tw-overflow-hidden tw-whitespace-nowrap">
|
||||
{{ account | userName }}
|
||||
</small>
|
||||
</div>
|
||||
</bit-menu>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div
|
||||
class="tw-ml-auto"
|
||||
#contentContainer
|
||||
[ngClass]="{ 'tw-hidden': contentContainer.childElementCount === 0 }"
|
||||
>
|
||||
<ng-content select="[slot=secondary]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
#tabsContainer
|
||||
class="-tw-mx-4 -tw-mb-px"
|
||||
[ngClass]="{ 'tw-hidden': tabsContainer.childElementCount === 0 }"
|
||||
>
|
||||
<ng-content select="[slot=tabs]"></ng-content>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
@if (selfHosted) {
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<span class="tw-break-all tw-px-4 tw-py-1 tw-text-left tw-text-muted">
|
||||
{{ hostname }}
|
||||
</span>
|
||||
}
|
||||
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
|
||||
<a bitMenuItem routerLink="/settings/account">
|
||||
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
|
||||
{{ "accountSettings" | i18n }}
|
||||
</a>
|
||||
<a bitMenuItem href="https://bitwarden.com/help/" target="_blank" rel="noreferrer">
|
||||
<i class="bwi bwi-fw bwi-question-circle" aria-hidden="true"></i>
|
||||
{{ "getHelp" | i18n }}
|
||||
</a>
|
||||
<a bitMenuItem href="https://bitwarden.com/download/" target="_blank" rel="noreferrer">
|
||||
<i class="bwi bwi-fw bwi-download" aria-hidden="true"></i>
|
||||
{{ "getApps" | i18n }}
|
||||
</a>
|
||||
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
|
||||
@if (canLock$ | async) {
|
||||
<button bitMenuItem type="button" (click)="lock()">
|
||||
<i class="bwi bwi-fw bwi-lock" aria-hidden="true"></i>
|
||||
{{ "lockNow" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
<button bitMenuItem type="button" (click)="logout()">
|
||||
<i class="bwi bwi-fw bwi-sign-out" aria-hidden="true"></i>
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</bit-menu>
|
||||
}
|
||||
|
||||
<ng-container slot="title-suffix">
|
||||
<ng-content select="[slot=title-suffix]"></ng-content>
|
||||
</ng-container>
|
||||
|
||||
<ng-container slot="secondary">
|
||||
<ng-content select="[slot=secondary]"></ng-content>
|
||||
</ng-container>
|
||||
|
||||
<ng-container slot="tabs">
|
||||
<ng-content select="[slot=tabs]"></ng-content>
|
||||
</ng-container>
|
||||
</bit-header>
|
||||
}
|
||||
|
||||
@@ -1,69 +1,62 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component, inject, input } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { User } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BannerModule, HeaderComponent } from "@bitwarden/components";
|
||||
|
||||
import { DynamicAvatarComponent } from "../../components/dynamic-avatar.component";
|
||||
import { SharedModule } from "../../shared";
|
||||
import { ProductSwitcherModule } from "../product-switcher/product-switcher.module";
|
||||
|
||||
// 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: "app-header",
|
||||
templateUrl: "./web-header.component.html",
|
||||
standalone: false,
|
||||
imports: [
|
||||
SharedModule,
|
||||
DynamicAvatarComponent,
|
||||
ProductSwitcherModule,
|
||||
BannerModule,
|
||||
HeaderComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class WebHeaderComponent {
|
||||
private route = inject(ActivatedRoute);
|
||||
private platformUtilsService = inject(PlatformUtilsService);
|
||||
private vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
private messagingService = inject(MessagingService);
|
||||
private accountService = inject(AccountService);
|
||||
|
||||
/**
|
||||
* Custom title that overrides the route data `titleId`
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() title: string;
|
||||
readonly title = input<string>();
|
||||
|
||||
/**
|
||||
* Icon to show before the title
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() icon: string;
|
||||
readonly icon = input<string>();
|
||||
|
||||
protected routeData$: Observable<{ titleId: string }>;
|
||||
protected account$: Observable<User & { id: UserId }>;
|
||||
protected canLock$: Observable<boolean>;
|
||||
protected selfHosted: boolean;
|
||||
protected hostname = location.hostname;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private messagingService: MessagingService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.routeData$ = this.route.data.pipe(
|
||||
map((params) => {
|
||||
return {
|
||||
titleId: params.titleId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.account$ = this.accountService.activeAccount$;
|
||||
this.canLock$ = this.vaultTimeoutSettingsService
|
||||
.availableVaultTimeoutActions$()
|
||||
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
|
||||
}
|
||||
protected routeData$: Observable<{ titleId: string }> = this.route.data.pipe(
|
||||
map((params) => {
|
||||
return {
|
||||
titleId: params.titleId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
protected account$: Observable<Account | null> = this.accountService.activeAccount$;
|
||||
protected canLock$: Observable<boolean> = this.vaultTimeoutSettingsService
|
||||
.availableVaultTimeoutActions$()
|
||||
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
|
||||
protected readonly selfHosted = this.platformUtilsService.isSelfHost();
|
||||
protected readonly hostname = globalThis.location.hostname;
|
||||
|
||||
protected lock() {
|
||||
this.messagingService.send("lockVault");
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, importProvidersFrom, Injectable, Input } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import {
|
||||
applicationConfig,
|
||||
componentWrapperDecorator,
|
||||
Meta,
|
||||
moduleMetadata,
|
||||
StoryObj,
|
||||
} from "@storybook/angular";
|
||||
import { BehaviorSubject, combineLatest, map, of } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import {
|
||||
AvatarModule,
|
||||
BreadcrumbsModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
InputModule,
|
||||
MenuModule,
|
||||
NavigationModule,
|
||||
TabsModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { DynamicAvatarComponent } from "../../components/dynamic-avatar.component";
|
||||
import { PreloadedEnglishI18nModule } from "../../core/tests";
|
||||
import { WebHeaderComponent } from "../header/web-header.component";
|
||||
|
||||
import { WebLayoutMigrationBannerService } from "./web-layout-migration-banner.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
class MockStateService {
|
||||
activeAccount$ = new BehaviorSubject("1").asObservable();
|
||||
accounts$ = new BehaviorSubject({ "1": { profile: { name: "Foo" } } }).asObservable();
|
||||
}
|
||||
|
||||
// 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: "product-switcher",
|
||||
template: `<button type="button" bitIconButton="bwi-filter" label="Switch products"></button>`,
|
||||
standalone: false,
|
||||
})
|
||||
class MockProductSwitcherComponent {}
|
||||
|
||||
// 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: "dynamic-avatar",
|
||||
template: `<bit-avatar [text]="name$ | async"></bit-avatar>`,
|
||||
imports: [CommonModule, AvatarModule],
|
||||
})
|
||||
class MockDynamicAvatarComponent implements Partial<DynamicAvatarComponent> {
|
||||
protected name$ = combineLatest([
|
||||
this.stateService.accounts$,
|
||||
this.stateService.activeAccount$,
|
||||
]).pipe(
|
||||
map(
|
||||
([accounts, activeAccount]) => accounts[activeAccount as keyof typeof accounts].profile.name,
|
||||
),
|
||||
);
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input()
|
||||
text?: string;
|
||||
|
||||
constructor(private stateService: MockStateService) {}
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "Web/Header",
|
||||
component: WebHeaderComponent,
|
||||
decorators: [
|
||||
componentWrapperDecorator(
|
||||
(story) => `<div class="tw-min-h-screen tw-flex-1 tw-p-6 tw-text-main">${story}</div>`,
|
||||
),
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
JslibModule,
|
||||
AvatarModule,
|
||||
BreadcrumbsModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
InputModule,
|
||||
MenuModule,
|
||||
TabsModule,
|
||||
TypographyModule,
|
||||
NavigationModule,
|
||||
MockDynamicAvatarComponent,
|
||||
],
|
||||
declarations: [WebHeaderComponent, MockProductSwitcherComponent],
|
||||
providers: [
|
||||
{ provide: StateService, useClass: MockStateService },
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: {
|
||||
activeAccount$: of({
|
||||
name: "Foobar Warden",
|
||||
}),
|
||||
} as Partial<AccountService>,
|
||||
},
|
||||
{
|
||||
provide: WebLayoutMigrationBannerService,
|
||||
useValue: {
|
||||
showBanner$: of(false),
|
||||
} as Partial<WebLayoutMigrationBannerService>,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: {
|
||||
isSelfHost() {
|
||||
return false;
|
||||
},
|
||||
} as Partial<PlatformUtilsService>,
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutSettingsService,
|
||||
useValue: {
|
||||
availableVaultTimeoutActions$() {
|
||||
return new BehaviorSubject([VaultTimeoutAction.Lock]).asObservable();
|
||||
},
|
||||
} as Partial<VaultTimeoutSettingsService>,
|
||||
},
|
||||
{
|
||||
provide: MessagingService,
|
||||
useValue: {
|
||||
send: (...args: any[]) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("MessagingService.send", args);
|
||||
},
|
||||
} as Partial<MessagingService>,
|
||||
},
|
||||
],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [
|
||||
importProvidersFrom(RouterModule.forRoot([], { useHash: true })),
|
||||
importProvidersFrom(PreloadedEnglishI18nModule),
|
||||
],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<WebHeaderComponent>;
|
||||
|
||||
export const KitchenSink: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<app-header title="LongTitleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" icon="bwi-bug">
|
||||
<bit-breadcrumbs slot="breadcrumbs">
|
||||
<bit-breadcrumb>Foo</bit-breadcrumb>
|
||||
<bit-breadcrumb>Bar</bit-breadcrumb>
|
||||
</bit-breadcrumbs>
|
||||
<input
|
||||
bitInput
|
||||
placeholder="Ask Jeeves"
|
||||
type="text"
|
||||
/>
|
||||
<button bitButton buttonType="primary">New</button>
|
||||
<button bitButton slot="secondary">Click Me 🎉</button>
|
||||
<bit-tab-nav-bar slot="tabs">
|
||||
<bit-tab-link route="">Foo</bit-tab-link>
|
||||
<bit-tab-link route="#bar">Bar</bit-tab-link>
|
||||
</bit-tab-nav-bar>
|
||||
</app-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Basic: Story = {
|
||||
render: (args: any) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<app-header title="Foobar" icon="bwi-bug"></app-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithLongTitle: Story = {
|
||||
render: (arg: any) => ({
|
||||
props: arg,
|
||||
template: `
|
||||
<app-header title="LongTitleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" icon="bwi-bug">
|
||||
<ng-container slot="title-suffix"><i class="bwi bwi-key"></i></ng-container>
|
||||
</app-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithBreadcrumbs: Story = {
|
||||
render: (args: any) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||
<bit-breadcrumbs slot="breadcrumbs">
|
||||
<bit-breadcrumb>Foo</bit-breadcrumb>
|
||||
<bit-breadcrumb>Bar</bit-breadcrumb>
|
||||
</bit-breadcrumbs>
|
||||
</app-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithSearch: Story = {
|
||||
render: (args: any) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||
<input
|
||||
bitInput
|
||||
placeholder="Ask Jeeves"
|
||||
type="text"
|
||||
/>
|
||||
</app-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithSecondaryContent: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||
<button bitButton slot="secondary">Click Me 🎉</button>
|
||||
</app-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithTabs: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||
<bit-tab-nav-bar slot="tabs">
|
||||
<bit-tab-link route="">Foo</bit-tab-link>
|
||||
<bit-tab-link route="#bar">Bar</bit-tab-link>
|
||||
</bit-tab-nav-bar>
|
||||
</app-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithTitleSuffixComponent: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||
<ng-container slot="title-suffix"><i class="bwi bwi-spinner bwi-spin"></i></ng-container>
|
||||
</app-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
35
libs/components/src/header/header.component.html
Normal file
35
libs/components/src/header/header.component.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<header
|
||||
class="-tw-mt-6 -tw-mx-8 tw-mb-3 tw-flex tw-flex-col tw-py-6 tw-px-8 has-[[data-tabs]:not(:empty)]:tw-border-0 has-[[data-tabs]:not(:empty)]:tw-border-b has-[[data-tabs]:not(:empty)]:tw-border-solid has-[[data-tabs]:not(:empty)]:tw-border-secondary-100 has-[[data-tabs]:not(:empty)]:tw-bg-background-alt has-[[data-tabs]:not(:empty)]:tw-pb-0"
|
||||
>
|
||||
<div class="tw-flex">
|
||||
<div class="tw-flex tw-min-w-0 tw-flex-1 tw-flex-col tw-gap-2">
|
||||
<ng-content select="[slot=breadcrumbs]"></ng-content>
|
||||
<h1
|
||||
bitTypography="h1"
|
||||
noMargin
|
||||
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex tw-gap-1"
|
||||
[title]="title()"
|
||||
>
|
||||
<div class="tw-truncate">
|
||||
@if (icon()) {
|
||||
<i class="bwi {{ icon() }}" aria-hidden="true"></i>
|
||||
}
|
||||
|
||||
{{ title() }}
|
||||
</div>
|
||||
<div><ng-content select="[slot=title-suffix]"></ng-content></div>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="tw-ml-auto tw-flex tw-flex-col tw-gap-4">
|
||||
<div class="tw-flex tw-min-w-max tw-items-center tw-justify-end tw-gap-2">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="tw-ml-auto empty:tw-hidden">
|
||||
<ng-content select="[slot=secondary]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-tabs class="-tw-mx-4 -tw-mb-px empty:tw-hidden">
|
||||
<ng-content select="[slot=tabs]"></ng-content>
|
||||
</div>
|
||||
</header>
|
||||
19
libs/components/src/header/header.component.ts
Normal file
19
libs/components/src/header/header.component.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "bit-header",
|
||||
templateUrl: "./header.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
})
|
||||
export class HeaderComponent {
|
||||
/**
|
||||
* Custom title that overrides the route data `titleId`
|
||||
*/
|
||||
readonly title = input.required<string>();
|
||||
|
||||
/**
|
||||
* Icon to show before the title
|
||||
*/
|
||||
readonly icon = input<string>();
|
||||
}
|
||||
190
libs/components/src/header/header.stories.ts
Normal file
190
libs/components/src/header/header.stories.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import {
|
||||
applicationConfig,
|
||||
componentWrapperDecorator,
|
||||
Meta,
|
||||
moduleMetadata,
|
||||
StoryObj,
|
||||
} from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
AvatarModule,
|
||||
BreadcrumbsModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
InputModule,
|
||||
MenuModule,
|
||||
NavigationModule,
|
||||
TabsModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { I18nMockService } from "../utils";
|
||||
|
||||
import { HeaderComponent } from "./header.component";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Header",
|
||||
component: HeaderComponent,
|
||||
decorators: [
|
||||
componentWrapperDecorator(
|
||||
(story) => `<div class="tw-min-h-screen tw-flex-1 tw-p-6 tw-text-main">${story}</div>`,
|
||||
),
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
|
||||
AvatarModule,
|
||||
BreadcrumbsModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
InputModule,
|
||||
MenuModule,
|
||||
NavigationModule,
|
||||
TabsModule,
|
||||
TypographyModule,
|
||||
],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
moreBreadcrumbs: "More breadcrumbs",
|
||||
loading: "Loading",
|
||||
});
|
||||
},
|
||||
},
|
||||
importProvidersFrom(
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{ path: "", redirectTo: "foo", pathMatch: "full" },
|
||||
{ path: "foo", component: HeaderComponent },
|
||||
{ path: "bar", component: HeaderComponent },
|
||||
],
|
||||
{ useHash: true },
|
||||
),
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<HeaderComponent>;
|
||||
|
||||
export const KitchenSink: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-header title="LongTitleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" icon="bwi-bug">
|
||||
<bit-breadcrumbs slot="breadcrumbs">
|
||||
<bit-breadcrumb>Foo</bit-breadcrumb>
|
||||
<bit-breadcrumb>Bar</bit-breadcrumb>
|
||||
</bit-breadcrumbs>
|
||||
<input
|
||||
bitInput
|
||||
placeholder="Ask Jeeves"
|
||||
type="text"
|
||||
/>
|
||||
<button type="button" bitIconButton="bwi-filter" label="Switch products"></button>
|
||||
<bit-avatar text="Will"></bit-avatar>
|
||||
<button bitButton buttonType="primary">New</button>
|
||||
<button bitButton slot="secondary">Click Me 🎉</button>
|
||||
<bit-tab-nav-bar slot="tabs">
|
||||
<bit-tab-link [route]="['foo']">Foo</bit-tab-link>
|
||||
<bit-tab-link [route]="['bar']">Bar</bit-tab-link>
|
||||
</bit-tab-nav-bar>
|
||||
</bit-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Basic: Story = {
|
||||
render: (args: any) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-header title="Foobar" icon="bwi-bug" />
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithLongTitle: Story = {
|
||||
render: (arg: any) => ({
|
||||
props: arg,
|
||||
template: /*html*/ `
|
||||
<bit-header title="LongTitleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" icon="bwi-bug">
|
||||
<ng-container slot="title-suffix"><i class="bwi bwi-key"></i></ng-container>
|
||||
</bit-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithBreadcrumbs: Story = {
|
||||
render: (args: any) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||
<bit-breadcrumbs slot="breadcrumbs">
|
||||
<bit-breadcrumb>Foo</bit-breadcrumb>
|
||||
<bit-breadcrumb>Bar</bit-breadcrumb>
|
||||
</bit-breadcrumbs>
|
||||
</bit-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithSearch: Story = {
|
||||
render: (args: any) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||
<input
|
||||
bitInput
|
||||
placeholder="Ask Jeeves"
|
||||
type="text"
|
||||
/>
|
||||
</bit-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithSecondaryContent: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||
<button bitButton slot="secondary">Click Me 🎉</button>
|
||||
</bit-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithTabs: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||
<bit-tab-nav-bar slot="tabs">
|
||||
<bit-tab-link [route]="['foo']">Foo</bit-tab-link>
|
||||
<bit-tab-link [route]="['bar']">Bar</bit-tab-link>
|
||||
</bit-tab-nav-bar>
|
||||
</bit-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithTitleSuffixComponent: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-header title="Foobar" icon="bwi-bug" class="tw-text-main">
|
||||
<ng-container slot="title-suffix"><i class="bwi bwi-spinner bwi-spin"></i></ng-container>
|
||||
</bit-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
1
libs/components/src/header/index.ts
Normal file
1
libs/components/src/header/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./header.component";
|
||||
@@ -19,6 +19,7 @@ export * from "./dialog";
|
||||
export * from "./disclosure";
|
||||
export * from "./drawer";
|
||||
export * from "./form-field";
|
||||
export * from "./header";
|
||||
export * from "./icon-button";
|
||||
export * from "./icon";
|
||||
export * from "./icon-tile";
|
||||
|
||||
Reference in New Issue
Block a user