From ec37e5e4d3650079318a2a07ed94947d87feccf3 Mon Sep 17 00:00:00 2001 From: Victoria League Date: Fri, 26 Apr 2024 09:35:32 -0400 Subject: [PATCH] [CL-219][CL-218][CL-217] Add new extension layout components (#8728) --- .storybook/main.ts | 2 + .storybook/tsconfig.json | 4 +- .storybook/typings.d.ts | 4 - .../popup/layout/popup-footer.component.html | 9 + .../popup/layout/popup-footer.component.ts | 9 + .../popup/layout/popup-header.component.html | 19 + .../popup/layout/popup-header.component.ts | 34 ++ .../platform/popup/layout/popup-layout.mdx | 138 +++++++ .../popup/layout/popup-layout.stories.ts | 367 ++++++++++++++++++ .../popup/layout/popup-page.component.html | 7 + .../popup/layout/popup-page.component.ts | 11 + .../popup-tab-navigation.component.html | 32 ++ .../layout/popup-tab-navigation.component.ts | 43 ++ apps/browser/src/popup/app.module.ts | 8 + apps/browser/src/popup/scss/base.scss | 6 +- apps/browser/tsconfig.json | 1 + libs/components/src/tw-theme.css | 8 + libs/components/tailwind.config.base.js | 5 + libs/components/tailwind.config.js | 1 + tsconfig.json | 16 +- 20 files changed, 711 insertions(+), 13 deletions(-) delete mode 100644 .storybook/typings.d.ts create mode 100644 apps/browser/src/platform/popup/layout/popup-footer.component.html create mode 100644 apps/browser/src/platform/popup/layout/popup-footer.component.ts create mode 100644 apps/browser/src/platform/popup/layout/popup-header.component.html create mode 100644 apps/browser/src/platform/popup/layout/popup-header.component.ts create mode 100644 apps/browser/src/platform/popup/layout/popup-layout.mdx create mode 100644 apps/browser/src/platform/popup/layout/popup-layout.stories.ts create mode 100644 apps/browser/src/platform/popup/layout/popup-page.component.html create mode 100644 apps/browser/src/platform/popup/layout/popup-page.component.ts create mode 100644 apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html create mode 100644 apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index c71a74c2a7..cb63ada550 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -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)", ], diff --git a/.storybook/tsconfig.json b/.storybook/tsconfig.json index 113cc5bcde..34acc9a740 100644 --- a/.storybook/tsconfig.json +++ b/.storybook/tsconfig.json @@ -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" diff --git a/.storybook/typings.d.ts b/.storybook/typings.d.ts deleted file mode 100644 index c94d67b1a2..0000000000 --- a/.storybook/typings.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.md" { - const content: string; - export default content; -} diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.html b/apps/browser/src/platform/popup/layout/popup-footer.component.html new file mode 100644 index 0000000000..2cbbca79c0 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.html @@ -0,0 +1,9 @@ + diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.ts b/apps/browser/src/platform/popup/layout/popup-footer.component.ts new file mode 100644 index 0000000000..826a1d1c60 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.ts @@ -0,0 +1,9 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "popup-footer", + templateUrl: "popup-footer.component.html", + standalone: true, + imports: [], +}) +export class PopupFooterComponent {} diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.html b/apps/browser/src/platform/popup/layout/popup-header.component.html new file mode 100644 index 0000000000..c0894f8168 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-header.component.html @@ -0,0 +1,19 @@ +
+
+
+ +

{{ pageTitle }}

+
+
+ +
+
+
diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.ts b/apps/browser/src/platform/popup/layout/popup-header.component.ts new file mode 100644 index 0000000000..f2f8eb95af --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-header.component.ts @@ -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(); + } +} diff --git a/apps/browser/src/platform/popup/layout/popup-layout.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx new file mode 100644 index 0000000000..91f7dab277 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx @@ -0,0 +1,138 @@ +import { Meta, Story, Canvas } from "@storybook/addon-docs"; + +import * as stories from "./popup-layout.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 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 + + +
This is content
+ +
+``` + +## 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 + + + + + + +``` + +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 + + + + +``` + +# 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. + + + + + +## Extension Page + +Examples of using just the `popup-page` component, without and with a footer. + + + + + + + + + +## Popped out + +When the browser extension is popped out, the "popout" button should not be passed to the header. + + + + diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts new file mode 100644 index 0000000000..1b10e50c0c --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -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: ` +
+ +
+ `, + standalone: true, +}) +class ExtensionContainerComponent {} + +@Component({ + selector: "vault-placeholder", + template: ` +
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item
+
vault item last item
+ `, + standalone: true, +}) +class VaultComponent {} + +@Component({ + selector: "generator-placeholder", + template: `
generator stuff here
`, + standalone: true, +}) +class GeneratorComponent {} + +@Component({ + selector: "send-placeholder", + template: `
send some stuff
`, + standalone: true, +}) +class SendComponent {} + +@Component({ + selector: "settings-placeholder", + template: `
change your settings
`, + standalone: true, +}) +class SettingsComponent {} + +@Component({ + selector: "mock-add-button", + template: ` + + `, + standalone: true, + imports: [ButtonModule], +}) +class MockAddButtonComponent {} + +@Component({ + selector: "mock-popout-button", + template: ` + + `, + standalone: true, + imports: [IconButtonModule], +}) +class MockPopoutButtonComponent {} + +@Component({ + selector: "mock-current-account", + template: ` + + `, + standalone: true, + imports: [AvatarModule], +}) +class MockCurrentAccountComponent {} + +@Component({ + selector: "mock-vault-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultPageComponent {} + +@Component({ + selector: "mock-vault-page-popped", + template: ` + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultPagePoppedComponent {} + +@Component({ + selector: "mock-generator-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + GeneratorComponent, + ], +}) +class MockGeneratorPageComponent {} + +@Component({ + selector: "mock-send-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + SendComponent, + ], +}) +class MockSendPageComponent {} + +@Component({ + selector: "mock-settings-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + SettingsComponent, + ], +}) +class MockSettingsPageComponent {} + +@Component({ + selector: "mock-vault-subpage", + template: ` + + + + + + + + + + + + + `, + 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; + +export const PopupTabNavigation: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + + + `, + }), +}; + +export const PopupPage: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + `, + }), +}; + +export const PopupPageWithFooter: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + `, + }), +}; + +export const PoppedOut: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` +
+ +
+ `, + }), +}; diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html new file mode 100644 index 0000000000..ba871d6319 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -0,0 +1,7 @@ + +
+
+ +
+
+ diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts new file mode 100644 index 0000000000..1223a6f418 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts @@ -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 {} diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html new file mode 100644 index 0000000000..a0ff252c6c --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -0,0 +1,32 @@ +
+ +
+ diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts new file mode 100644 index 0000000000..3a275454d9 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -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", + }, + ]; +} diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 5718542b01..2fb582d693 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -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, diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 73da455941..80c7536087 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -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; diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 505f1533ae..e1bf2b7211 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "moduleResolution": "node", "noImplicitAny": true, + "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "module": "ES2020", diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 72e8e1e5e8..00ab2ff717 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -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; } diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index b76f25eae7..12af316b38 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -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"), diff --git a/libs/components/tailwind.config.js b/libs/components/tailwind.config.js index 987b969e8f..7a53c82ec5 100644 --- a/libs/components/tailwind.config.js +++ b/libs/components/tailwind.config.js @@ -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", ]; diff --git a/tsconfig.json b/tsconfig.json index ab3f8861a9..60dc9d223e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" + ] }