[CL-219][CL-218][CL-217] Add new extension layout components (#8728)
This commit is contained in:
parent
2fa4c6e4f9
commit
ec37e5e4d3
|
@ -9,6 +9,8 @@ const config: StorybookConfig = {
|
|||
"../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../apps/web/src/**/*.mdx",
|
||||
"../apps/web/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../apps/browser/src/**/*.mdx",
|
||||
"../apps/browser/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../bitwarden_license/bit-web/src/**/*.mdx",
|
||||
"../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
],
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
{
|
||||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest", "chrome"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": ["../src/test.setup.ts", "../apps/src/**/*.spec.ts", "../libs/**/*.spec.ts"],
|
||||
"exclude": ["../src/test.setup.ts", "../apps/**/*.spec.ts", "../libs/**/*.spec.ts"],
|
||||
"files": [
|
||||
"./typings.d.ts",
|
||||
"./preview.tsx",
|
||||
"../libs/components/src/main.ts",
|
||||
"../libs/components/src/polyfills.ts"
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
declare module "*.md" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<footer
|
||||
class="tw-p-3 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
|
||||
>
|
||||
<div class="tw-max-w-screen-sm tw-mx-auto">
|
||||
<div class="tw-flex tw-justify-start tw-gap-2">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
|
@ -0,0 +1,9 @@
|
|||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "popup-footer",
|
||||
templateUrl: "popup-footer.component.html",
|
||||
standalone: true,
|
||||
imports: [],
|
||||
})
|
||||
export class PopupFooterComponent {}
|
|
@ -0,0 +1,19 @@
|
|||
<header
|
||||
class="tw-p-4 tw-border-0 tw-border-solid tw-border-b tw-border-secondary-300 tw-bg-background"
|
||||
>
|
||||
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
|
||||
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
||||
<button
|
||||
bitIconButton="bwi-back"
|
||||
type="button"
|
||||
*ngIf="showBackButton"
|
||||
[title]="'back' | i18n"
|
||||
[ariaLabel]="'back' | i18n"
|
||||
></button>
|
||||
<h1 bitTypography="h3" class="!tw-mb-0.5 tw-text-headers">{{ pageTitle }}</h1>
|
||||
</div>
|
||||
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
|
@ -0,0 +1,34 @@
|
|||
import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { CommonModule, Location } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { IconButtonModule, TypographyModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "popup-header",
|
||||
templateUrl: "popup-header.component.html",
|
||||
standalone: true,
|
||||
imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule],
|
||||
})
|
||||
export class PopupHeaderComponent {
|
||||
/** Display the back button, which uses Location.back() to go back one page in history */
|
||||
@Input()
|
||||
get showBackButton() {
|
||||
return this._showBackButton;
|
||||
}
|
||||
set showBackButton(value: BooleanInput) {
|
||||
this._showBackButton = coerceBooleanProperty(value);
|
||||
}
|
||||
|
||||
private _showBackButton = false;
|
||||
|
||||
/** Title string that will be inserted as an h1 */
|
||||
@Input({ required: true }) pageTitle: string;
|
||||
|
||||
constructor(private location: Location) {}
|
||||
|
||||
back() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
import { Meta, Story, Canvas } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./popup-layout.stories";
|
||||
|
||||
<Meta of={stories} />
|
||||
|
||||
Please note that because these stories use `router-outlet`, there are issues with rendering content
|
||||
when Light & Dark mode is selected. The stories are best viewed by selecting one color mode.
|
||||
|
||||
# Popup Tab Navigation
|
||||
|
||||
The popup tab navigation component composes together the popup page and the bottom tab navigation
|
||||
footer. This component is intended to be used a level _above_ each extension tab's page code.
|
||||
|
||||
The navigation footer contains the 4 main page links for the browser extension. It uses the Angular
|
||||
router to determine which page is currently active, and style the button appropriately. Clicking on
|
||||
the buttons will navigate to the correct route. The navigation footer has a max-width built in so
|
||||
that the page looks nice when the extension is popped out.
|
||||
|
||||
Long button names will be ellipsed.
|
||||
|
||||
Usage example:
|
||||
|
||||
```html
|
||||
<popup-tab-navigation>
|
||||
<popup-page></popup-page>
|
||||
</popup-tab-navigation>
|
||||
```
|
||||
|
||||
# Popup Page
|
||||
|
||||
The popup page handles positioning a page's `header` and `footer` elements, and inserting the rest
|
||||
of the content into the `main` element with scroll. There is also a max-width built in so that the
|
||||
page looks nice when the extension is popped out.
|
||||
|
||||
**Slots**
|
||||
|
||||
- `header`
|
||||
- Use `popup-header` component.
|
||||
- Every page should have a header.
|
||||
- `footer`
|
||||
- Use the `popup-footer` component.
|
||||
- Not every page will have a footer.
|
||||
- default
|
||||
- Whatever content you want in `main`.
|
||||
|
||||
Basic usage example:
|
||||
|
||||
```html
|
||||
<popup-page>
|
||||
<popup-header slot="header"></popup-header>
|
||||
<div>This is content</div>
|
||||
<popup-footer slot="footer"></popup-footer>
|
||||
</popup-page>
|
||||
```
|
||||
|
||||
## Popup header
|
||||
|
||||
**Args**
|
||||
|
||||
- `pageTitle`: required
|
||||
- Inserts title as an `h1`.
|
||||
- `showBackButton`: optional, defaults to `false`
|
||||
- Toggles the back button to appear. The back button uses `Location.back()` to navigate back one
|
||||
page in history.
|
||||
|
||||
**Slots**
|
||||
|
||||
- `end`
|
||||
- Use to insert one or more interactive elements.
|
||||
- The header handles the spacing between elements passed to the `end` slot.
|
||||
|
||||
Usage example:
|
||||
|
||||
```html
|
||||
<popup-header pageTitle="Test" showBackButton>
|
||||
<ng-container slot="end">
|
||||
<button></button>
|
||||
<button></button>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
```
|
||||
|
||||
Common interactive elements to insert into the `end` slot are:
|
||||
|
||||
- `app-current-account`: shows current account and switcher
|
||||
- `app-pop-out`: shows popout button when the extension is not already popped out
|
||||
- "Add" button: this can be accomplished with the Button component and any custom functionality for
|
||||
that particular page
|
||||
|
||||
## Popup footer
|
||||
|
||||
Popup footer should be used when the page displays action buttons. It functions similarly to the
|
||||
Dialog footer in that the calling code is responsible for passing in the different buttons that need
|
||||
to be rendered.
|
||||
|
||||
Usage example:
|
||||
|
||||
```html
|
||||
<popup-footer>
|
||||
<button bitButton buttonType="primary">Save</button>
|
||||
<button bitButton buttonType="secondary">Cancel</button>
|
||||
</popup-footer>
|
||||
```
|
||||
|
||||
# Page types
|
||||
|
||||
There are a few types of pages that are used in the browser extension.
|
||||
|
||||
View the story source code to see examples of how to construct these types of pages.
|
||||
|
||||
## Extension Tab
|
||||
|
||||
Example of wrapping an extension page in the `popup-tab-navigation` component.
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.PopupTabNavigation} />
|
||||
</Canvas>
|
||||
|
||||
## Extension Page
|
||||
|
||||
Examples of using just the `popup-page` component, without and with a footer.
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.PopupPage} />
|
||||
</Canvas>
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.PopupPageWithFooter} />
|
||||
</Canvas>
|
||||
|
||||
## Popped out
|
||||
|
||||
When the browser extension is popped out, the "popout" button should not be passed to the header.
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.PoppedOut} />
|
||||
</Canvas>
|
|
@ -0,0 +1,367 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component, importProvidersFrom } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
AvatarModule,
|
||||
ButtonModule,
|
||||
I18nMockService,
|
||||
IconButtonModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PopupFooterComponent } from "./popup-footer.component";
|
||||
import { PopupHeaderComponent } from "./popup-header.component";
|
||||
import { PopupPageComponent } from "./popup-page.component";
|
||||
import { PopupTabNavigationComponent } from "./popup-tab-navigation.component";
|
||||
|
||||
@Component({
|
||||
selector: "extension-container",
|
||||
template: `
|
||||
<div class="tw-h-[640px] tw-w-[380px] tw-border tw-border-solid tw-border-secondary-300">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
standalone: true,
|
||||
})
|
||||
class ExtensionContainerComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "vault-placeholder",
|
||||
template: `
|
||||
<div class="tw-mb-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
||||
<div class="tw-my-8 tw-text-main">vault item last item</div>
|
||||
`,
|
||||
standalone: true,
|
||||
})
|
||||
class VaultComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "generator-placeholder",
|
||||
template: ` <div class="tw-text-main">generator stuff here</div> `,
|
||||
standalone: true,
|
||||
})
|
||||
class GeneratorComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "send-placeholder",
|
||||
template: ` <div class="tw-text-main">send some stuff</div> `,
|
||||
standalone: true,
|
||||
})
|
||||
class SendComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "settings-placeholder",
|
||||
template: ` <div class="tw-text-main">change your settings</div> `,
|
||||
standalone: true,
|
||||
})
|
||||
class SettingsComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "mock-add-button",
|
||||
template: `
|
||||
<button bitButton buttonType="primary" type="button">
|
||||
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||
Add
|
||||
</button>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [ButtonModule],
|
||||
})
|
||||
class MockAddButtonComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "mock-popout-button",
|
||||
template: `
|
||||
<button
|
||||
bitIconButton="bwi-popout"
|
||||
size="small"
|
||||
type="button"
|
||||
title="Pop out"
|
||||
aria-label="Pop out"
|
||||
></button>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [IconButtonModule],
|
||||
})
|
||||
class MockPopoutButtonComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "mock-current-account",
|
||||
template: `
|
||||
<button class="tw-bg-transparent tw-border-none" type="button">
|
||||
<bit-avatar text="Ash Ketchum" size="small"></bit-avatar>
|
||||
</button>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [AvatarModule],
|
||||
})
|
||||
class MockCurrentAccountComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "mock-vault-page",
|
||||
template: `
|
||||
<popup-page>
|
||||
<popup-header slot="header" pageTitle="Test">
|
||||
<ng-container slot="end">
|
||||
<mock-add-button></mock-add-button>
|
||||
<mock-popout-button></mock-popout-button>
|
||||
<mock-current-account></mock-current-account>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<vault-placeholder></vault-placeholder>
|
||||
</popup-page>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
MockAddButtonComponent,
|
||||
MockPopoutButtonComponent,
|
||||
MockCurrentAccountComponent,
|
||||
VaultComponent,
|
||||
],
|
||||
})
|
||||
class MockVaultPageComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "mock-vault-page-popped",
|
||||
template: `
|
||||
<popup-page>
|
||||
<popup-header slot="header" pageTitle="Test">
|
||||
<ng-container slot="end">
|
||||
<mock-add-button></mock-add-button>
|
||||
<mock-current-account></mock-current-account>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<vault-placeholder></vault-placeholder>
|
||||
</popup-page>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
MockAddButtonComponent,
|
||||
MockPopoutButtonComponent,
|
||||
MockCurrentAccountComponent,
|
||||
VaultComponent,
|
||||
],
|
||||
})
|
||||
class MockVaultPagePoppedComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "mock-generator-page",
|
||||
template: `
|
||||
<popup-page>
|
||||
<popup-header slot="header" pageTitle="Test">
|
||||
<ng-container slot="end">
|
||||
<mock-add-button></mock-add-button>
|
||||
<mock-popout-button></mock-popout-button>
|
||||
<mock-current-account></mock-current-account>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<generator-placeholder></generator-placeholder>
|
||||
</popup-page>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
MockAddButtonComponent,
|
||||
MockPopoutButtonComponent,
|
||||
MockCurrentAccountComponent,
|
||||
GeneratorComponent,
|
||||
],
|
||||
})
|
||||
class MockGeneratorPageComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "mock-send-page",
|
||||
template: `
|
||||
<popup-page>
|
||||
<popup-header slot="header" pageTitle="Test">
|
||||
<ng-container slot="end">
|
||||
<mock-add-button></mock-add-button>
|
||||
<mock-popout-button></mock-popout-button>
|
||||
<mock-current-account></mock-current-account>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<send-placeholder></send-placeholder>
|
||||
</popup-page>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
MockAddButtonComponent,
|
||||
MockPopoutButtonComponent,
|
||||
MockCurrentAccountComponent,
|
||||
SendComponent,
|
||||
],
|
||||
})
|
||||
class MockSendPageComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "mock-settings-page",
|
||||
template: `
|
||||
<popup-page>
|
||||
<popup-header slot="header" pageTitle="Test">
|
||||
<ng-container slot="end">
|
||||
<mock-add-button></mock-add-button>
|
||||
<mock-popout-button></mock-popout-button>
|
||||
<mock-current-account></mock-current-account>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<settings-placeholder></settings-placeholder>
|
||||
</popup-page>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
MockAddButtonComponent,
|
||||
MockPopoutButtonComponent,
|
||||
MockCurrentAccountComponent,
|
||||
SettingsComponent,
|
||||
],
|
||||
})
|
||||
class MockSettingsPageComponent {}
|
||||
|
||||
@Component({
|
||||
selector: "mock-vault-subpage",
|
||||
template: `
|
||||
<popup-page>
|
||||
<popup-header slot="header" pageTitle="Test" showBackButton>
|
||||
<ng-container slot="end">
|
||||
<mock-popout-button></mock-popout-button>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<vault-placeholder></vault-placeholder>
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton buttonType="primary">Save</button>
|
||||
<button bitButton buttonType="secondary">Cancel</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopupFooterComponent,
|
||||
ButtonModule,
|
||||
MockAddButtonComponent,
|
||||
MockPopoutButtonComponent,
|
||||
MockCurrentAccountComponent,
|
||||
VaultComponent,
|
||||
],
|
||||
})
|
||||
class MockVaultSubpageComponent {}
|
||||
|
||||
export default {
|
||||
title: "Browser/Popup Layout",
|
||||
component: PopupPageComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
PopupTabNavigationComponent,
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
ExtensionContainerComponent,
|
||||
MockVaultSubpageComponent,
|
||||
MockVaultPageComponent,
|
||||
MockSendPageComponent,
|
||||
MockGeneratorPageComponent,
|
||||
MockSettingsPageComponent,
|
||||
MockVaultPagePoppedComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
back: "Back",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [
|
||||
importProvidersFrom(
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{ path: "", redirectTo: "vault", pathMatch: "full" },
|
||||
{ path: "vault", component: MockVaultPageComponent },
|
||||
{ path: "generator", component: MockGeneratorPageComponent },
|
||||
{ path: "send", component: MockSendPageComponent },
|
||||
{ path: "settings", component: MockSettingsPageComponent },
|
||||
// in case you are coming from a story that also uses the router
|
||||
{ path: "**", redirectTo: "vault" },
|
||||
],
|
||||
{ useHash: true },
|
||||
),
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<PopupPageComponent>;
|
||||
|
||||
export const PopupTabNavigation: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /* HTML */ `
|
||||
<extension-container>
|
||||
<popup-tab-navigation>
|
||||
<router-outlet></router-outlet>
|
||||
</popup-tab-navigation>
|
||||
</extension-container>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const PopupPage: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /* HTML */ `
|
||||
<extension-container>
|
||||
<mock-vault-page></mock-vault-page>
|
||||
</extension-container>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const PopupPageWithFooter: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /* HTML */ `
|
||||
<extension-container>
|
||||
<mock-vault-subpage></mock-vault-subpage>
|
||||
</extension-container>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const PoppedOut: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /* HTML */ `
|
||||
<div class="tw-h-[640px] tw-w-[900px] tw-border tw-border-solid tw-border-secondary-300">
|
||||
<mock-vault-page-popped></mock-vault-page-popped>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
<ng-content select="[slot=header]"></ng-content>
|
||||
<main class="tw-bg-background-alt tw-p-3 tw-flex-1 tw-overflow-y-auto">
|
||||
<div class="tw-max-w-screen-sm tw-mx-auto">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</main>
|
||||
<ng-content select="[slot=footer]"></ng-content>
|
|
@ -0,0 +1,11 @@
|
|||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "popup-page",
|
||||
templateUrl: "popup-page.component.html",
|
||||
standalone: true,
|
||||
host: {
|
||||
class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto",
|
||||
},
|
||||
})
|
||||
export class PopupPageComponent {}
|
|
@ -0,0 +1,32 @@
|
|||
<div class="tw-h-full tw-overflow-y-auto [&>*]:tw-h-full [&>*]:tw-overflow-y-auto">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<footer class="tw-bg-background tw-border-0 tw-border-t tw-border-secondary-300 tw-border-solid">
|
||||
<div class="tw-max-w-screen-sm tw-mx-auto">
|
||||
<div class="tw-flex tw-flex-1">
|
||||
<a
|
||||
*ngFor="let button of navButtons"
|
||||
class="tw-group tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 tw-pt-3 tw-w-1/4 tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-500"
|
||||
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
|
||||
[title]="button.label"
|
||||
[routerLink]="button.page"
|
||||
routerLinkActive
|
||||
#rla="routerLinkActive"
|
||||
ariaCurrentWhenActive="page"
|
||||
>
|
||||
<i *ngIf="!rla.isActive" class="bwi bwi-lg bwi-{{ button.iconKey }}" aria-hidden="true"></i>
|
||||
<i
|
||||
*ngIf="rla.isActive"
|
||||
class="bwi bwi-lg bwi-{{ button.iconKeyActive }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span
|
||||
class="tw-truncate tw-max-w-full"
|
||||
[ngClass]="!rla.isActive && 'group-hover:tw-underline'"
|
||||
>
|
||||
{{ button.label }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
|
@ -0,0 +1,43 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { LinkModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "popup-tab-navigation",
|
||||
templateUrl: "popup-tab-navigation.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, LinkModule, RouterModule],
|
||||
host: {
|
||||
class: "tw-block tw-h-full tw-w-full tw-flex tw-flex-col",
|
||||
},
|
||||
})
|
||||
export class PopupTabNavigationComponent {
|
||||
navButtons = [
|
||||
{
|
||||
label: "Vault",
|
||||
page: "/vault",
|
||||
iconKey: "lock",
|
||||
iconKeyActive: "lock-f",
|
||||
},
|
||||
{
|
||||
label: "Generator",
|
||||
page: "/generator",
|
||||
iconKey: "generate",
|
||||
iconKeyActive: "generate-f",
|
||||
},
|
||||
{
|
||||
label: "Send",
|
||||
page: "/send",
|
||||
iconKey: "send",
|
||||
iconKeyActive: "send-f",
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
page: "/settings",
|
||||
iconKey: "cog",
|
||||
iconKeyActive: "cog-f",
|
||||
},
|
||||
];
|
||||
}
|
|
@ -36,6 +36,10 @@ import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
|||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||
import { HeaderComponent } from "../platform/popup/header.component";
|
||||
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../platform/popup/layout/popup-page.component";
|
||||
import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component";
|
||||
import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
|
||||
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
||||
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
||||
|
@ -108,6 +112,10 @@ import "../platform/popup/locales";
|
|||
AccountComponent,
|
||||
ButtonModule,
|
||||
ExportScopeCalloutComponent,
|
||||
PopupPageComponent,
|
||||
PopupTabNavigationComponent,
|
||||
PopupFooterComponent,
|
||||
PopupHeaderComponent,
|
||||
],
|
||||
declarations: [
|
||||
ActionButtonsComponent,
|
||||
|
|
|
@ -68,7 +68,7 @@ img {
|
|||
border: none;
|
||||
}
|
||||
|
||||
a {
|
||||
a:not(popup-page a, popup-tab-navigation a) {
|
||||
text-decoration: none;
|
||||
|
||||
@include themify($themes) {
|
||||
|
@ -171,7 +171,7 @@ cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb,
|
|||
}
|
||||
}
|
||||
|
||||
header:not(bit-callout header, bit-dialog header) {
|
||||
header:not(bit-callout header, bit-dialog header, popup-page header) {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
|
||||
|
@ -448,7 +448,7 @@ app-root {
|
|||
}
|
||||
}
|
||||
|
||||
main {
|
||||
main:not(popup-page main) {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
bottom: 0;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "ES2020",
|
||||
|
|
|
@ -17,7 +17,11 @@
|
|||
--color-background-alt3: 18 82 163;
|
||||
--color-background-alt4: 13 60 119;
|
||||
|
||||
/* Can only be used behind the extension refresh flag */
|
||||
--color-primary-100: 200 217 249;
|
||||
--color-primary-300: 103 149 232;
|
||||
/* Can only be used behind the extension refresh flag */
|
||||
--color-primary-500: 23 93 220;
|
||||
--color-primary-600: 23 93 220;
|
||||
--color-primary-700: 18 82 163;
|
||||
|
||||
|
@ -43,6 +47,7 @@
|
|||
--color-text-contrast: 255 255 255;
|
||||
--color-text-alt2: 255 255 255;
|
||||
--color-text-code: 192 17 118;
|
||||
--color-text-headers: 2 15 102;
|
||||
|
||||
--tw-ring-offset-color: #ffffff;
|
||||
}
|
||||
|
@ -60,7 +65,9 @@
|
|||
--color-background-alt3: 47 52 61;
|
||||
--color-background-alt4: 16 18 21;
|
||||
|
||||
--color-primary-100: 8 31 73;
|
||||
--color-primary-300: 23 93 220;
|
||||
--color-primary-500: 54 117 232;
|
||||
--color-primary-600: 106 153 240;
|
||||
--color-primary-700: 180 204 249;
|
||||
|
||||
|
@ -86,6 +93,7 @@
|
|||
--color-text-contrast: 25 30 38;
|
||||
--color-text-alt2: 255 255 255;
|
||||
--color-text-code: 240 141 199;
|
||||
--color-text-headers: 226 227 228;
|
||||
|
||||
--tw-ring-offset-color: #1f242e;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,11 @@ module.exports = {
|
|||
current: colors.current,
|
||||
black: colors.black,
|
||||
primary: {
|
||||
// Can only be used behind the extension refresh flag
|
||||
100: rgba("--color-primary-100"),
|
||||
300: rgba("--color-primary-300"),
|
||||
// Can only be used behind the extension refresh flag
|
||||
500: rgba("--color-primary-500"),
|
||||
600: rgba("--color-primary-600"),
|
||||
700: rgba("--color-primary-700"),
|
||||
},
|
||||
|
@ -69,6 +73,7 @@ module.exports = {
|
|||
main: rgba("--color-text-main"),
|
||||
muted: rgba("--color-text-muted"),
|
||||
contrast: rgba("--color-text-contrast"),
|
||||
headers: rgba("--color-text-headers"),
|
||||
alt2: rgba("--color-text-alt2"),
|
||||
code: rgba("--color-text-code"),
|
||||
success: rgba("--color-success-600"),
|
||||
|
|
|
@ -5,6 +5,7 @@ config.content = [
|
|||
"libs/components/src/**/*.{html,ts,mdx}",
|
||||
"libs/auth/src/**/*.{html,ts,mdx}",
|
||||
"apps/web/src/**/*.{html,ts,mdx}",
|
||||
"apps/browser/src/**/*.{html,ts,mdx}",
|
||||
"bitwarden_license/bit-web/src/**/*.{html,ts,mdx}",
|
||||
".storybook/preview.tsx",
|
||||
];
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"noImplicitAny": true,
|
||||
"target": "ES2016",
|
||||
"module": "ES2020",
|
||||
"lib": ["es5", "es6", "es7", "dom"],
|
||||
"lib": ["es5", "es6", "es7", "dom", "ES2021"],
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
|
@ -38,6 +38,16 @@
|
|||
],
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"include": ["apps/web/src/**/*", "libs/*/src/**/*", "bitwarden_license/bit-web/src/**/*"],
|
||||
"exclude": ["apps/web/src/**/*.spec.ts", "libs/*/src/**/*.spec.ts", "**/*.spec-util.ts"]
|
||||
"include": [
|
||||
"apps/web/src/**/*",
|
||||
"apps/browser/src/**/*",
|
||||
"libs/*/src/**/*",
|
||||
"bitwarden_license/bit-web/src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"apps/web/src/**/*.spec.ts",
|
||||
"apps/browser/src/**/*.spec.ts",
|
||||
"libs/*/src/**/*.spec.ts",
|
||||
"**/*.spec-util.ts"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue