diff --git a/.storybook/main.ts b/.storybook/main.ts index cb63ada550..26eee201f9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -4,6 +4,7 @@ import remarkGfm from "remark-gfm"; const config: StorybookConfig = { stories: [ + "../libs/auth/src/**/*.mdx", "../libs/auth/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/components/src/**/*.mdx", "../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)", diff --git a/apps/web/src/app/auth/anon-layout-wrapper.component.html b/apps/web/src/app/auth/anon-layout-wrapper.component.html new file mode 100644 index 0000000000..26cab30809 --- /dev/null +++ b/apps/web/src/app/auth/anon-layout-wrapper.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/app/auth/anon-layout-wrapper.component.ts b/apps/web/src/app/auth/anon-layout-wrapper.component.ts new file mode 100644 index 0000000000..e39a8e11a9 --- /dev/null +++ b/apps/web/src/app/auth/anon-layout-wrapper.component.ts @@ -0,0 +1,34 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute, RouterModule } from "@angular/router"; + +import { AnonLayoutComponent } from "@bitwarden/auth/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Icon } from "@bitwarden/components"; + +@Component({ + standalone: true, + templateUrl: "anon-layout-wrapper.component.html", + imports: [AnonLayoutComponent, RouterModule], +}) +export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { + protected pageTitle: string; + protected pageSubtitle: string; + protected pageIcon: Icon; + + constructor( + private route: ActivatedRoute, + private i18nService: I18nService, + ) { + this.pageTitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageTitle"]); + this.pageSubtitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageSubtitle"]); + this.pageIcon = this.route.snapshot.firstChild.data["pageIcon"]; // don't translate + } + + ngOnInit() { + document.body.classList.add("layout_frontend"); + } + + ngOnDestroy() { + document.body.classList.remove("layout_frontend"); + } +} diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index a944f9dd67..e80bf6a834 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -4,6 +4,7 @@ const config = require("../../libs/components/tailwind.config.base"); config.content = [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", + "../../libs/auth/src/**/*.{html,ts}", "../../bitwarden_license/bit-web/src/**/*.{html,ts}", ]; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 1f583edf20..55da36d9bf 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -1,5 +1,5 @@
@@ -13,8 +13,10 @@

{{ subtitle }}

-
-
+
+
diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index d247a010bf..106844fb5a 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -5,7 +5,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { IconModule, Icon } from "../../../../components/src/icon"; import { TypographyModule } from "../../../../components/src/typography"; -import { BitwardenLogo } from "../../icons/bitwarden-logo"; +import { BitwardenLogo } from "../icons/bitwarden-logo.icon"; @Component({ standalone: true, diff --git a/libs/auth/src/angular/anon-layout/anon-layout.mdx b/libs/auth/src/angular/anon-layout/anon-layout.mdx new file mode 100644 index 0000000000..c604c02f03 --- /dev/null +++ b/libs/auth/src/angular/anon-layout/anon-layout.mdx @@ -0,0 +1,118 @@ +import { Meta, Story, Controls } from "@storybook/addon-docs"; + +import * as stories from "./anon-layout.stories"; + + + +# AnonLayout Component + +The Auth-owned AnonLayoutComponent is to be used for unauthenticated pages, where we don't know who +the user is (this includes viewing a Send). + +--- + +### Incorrect Usage ❌ + +The AnonLayoutComponent is **not** to be implemented by every component that uses it in that +component's template directly. For example, if you have a component template called +`example.component.html`, and you want it to use the AnonLayoutComponent, you will **not** be +writing: + +```html + + + +
Example component content
+
+``` + +### Correct Usage ✅ + +Instead the AnonLayoutComponent is implemented solely in the router via routable composition, which +gives us the advantages of nested routes in Angular. + +To allow for routable composition, Auth will also provide a wrapper component in each client, called +AnonLayout**Wrapper**Component. + +For clarity: + +- AnonLayoutComponent = the Auth-owned library component - `` +- AnonLayout**Wrapper**Component = the client-specific wrapper component to be used in a client + routing module + +The AnonLayout**Wrapper**Component embeds the AnonLayoutComponent along with the router outlets: + +```html + + + + + + +``` + +To implement, the developer does not need to work with the base AnonLayoutComponent directly. The +devoloper simply uses the AnonLayout**Wrapper**Component in `oss-routing.module.ts` (for Web, for +example) to construct the page via routable composition: + +```javascript +// File: oss-routing.module.ts + +{ + path: "", + component: AnonLayoutWrapperComponent, // Wrapper component + children: [ + { + path: "sample-route", // replace with your route + children: [ + { + path: "", + component: MyPrimaryComponent, // replace with your component + }, + { + path: "", + component: MySecondaryComponent, // replace with your component (or remove this secondary outlet object entirely if not needed) + outlet: "secondary", + }, + ], + data: { + pageTitle: "logIn", // example of a translation key from messages.json + pageSubtitle: "loginWithMasterPassword", // example of a translation key from messages.json + pageIcon: LockIcon, // example of an icon to pass in + }, + }, + ], + }, +``` + +And if the AnonLayout**Wrapper**Component is already being used in your client's routing module, +then your work will be as simple as just adding another child route under the `children` array. + +### Data Properties + +In the `oss-routing.module.ts` example above, notice the data properties being passed in: + +- For the `pageTitle` and `pageSubtitle` - pass in a translation key from `messages.json`. +- For the `pageIcon` - import an icon (of type `Icon`) into the router file and use the icon + directly. + +All 3 of these properties are optional. + +```javascript +import { LockIcon } from "@bitwarden/auth/angular"; + +// ... + +{ + // ... + data: { + pageTitle: "logIn", + pageSubtitle: "loginWithMasterPassword", + pageIcon: LockIcon, + }, +} +``` + +--- + + diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts index daba5b5e53..61a395b155 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -3,12 +3,12 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule } from "../../../../components/src/button"; -import { IconLock } from "../../icons/icon-lock"; +import { LockIcon } from "../icons"; import { AnonLayoutComponent } from "./anon-layout.component"; class MockPlatformUtilsService implements Partial { - getApplicationVersion = () => Promise.resolve("Version 2023.1.1"); + getApplicationVersion = () => Promise.resolve("Version 2024.1.1"); } export default { @@ -28,7 +28,7 @@ export default { args: { title: "The Page Title", subtitle: "The subtitle (optional)", - icon: IconLock, + icon: LockIcon, }, } as Meta; @@ -38,14 +38,13 @@ export const WithPrimaryContent: Story = { render: (args) => ({ props: args, template: - /** - * The projected content (i.e. the
) and styling below is just a - * sample and could be replaced with any content and styling - */ + // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. ` -
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
+
+
Primary Projected Content Area (customizable)
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
+
`, }), @@ -55,15 +54,16 @@ export const WithSecondaryContent: Story = { render: (args) => ({ props: args, template: - // Notice that slot="secondary" is requred to project any secondary content: + // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. + // Notice that slot="secondary" is requred to project any secondary content. ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
+
Secondary Projected Content (optional)
@@ -75,14 +75,16 @@ export const WithSecondaryContent: Story = { export const WithLongContent: Story = { render: (args) => ({ props: args, - template: ` + template: + // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. + ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.
-
+
Secondary Projected Content (optional)

Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Expedita, quod est?

@@ -95,9 +97,11 @@ export const WithLongContent: Story = { export const WithIcon: Story = { render: (args) => ({ props: args, - template: ` + template: + // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. + ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
diff --git a/libs/auth/src/icons/bitwarden-logo.ts b/libs/auth/src/angular/icons/bitwarden-logo.icon.ts similarity index 100% rename from libs/auth/src/icons/bitwarden-logo.ts rename to libs/auth/src/angular/icons/bitwarden-logo.icon.ts diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 7bb3f57579..d71e2e6efd 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -1 +1,3 @@ +export * from "./bitwarden-logo.icon"; +export * from "./lock.icon"; export * from "./user-verification-biometrics-fingerprint.icon"; diff --git a/libs/auth/src/icons/icon-lock.ts b/libs/auth/src/angular/icons/lock.icon.ts similarity index 98% rename from libs/auth/src/icons/icon-lock.ts rename to libs/auth/src/angular/icons/lock.icon.ts index 61330fe0df..b567c213f7 100644 --- a/libs/auth/src/icons/icon-lock.ts +++ b/libs/auth/src/angular/icons/lock.icon.ts @@ -1,6 +1,6 @@ import { svgIcon } from "@bitwarden/components"; -export const IconLock = svgIcon` +export const LockIcon = svgIcon` diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index c93bf1c1d3..067ed63b8e 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -5,6 +5,7 @@ // icons export * from "./icons"; +export * from "./anon-layout/anon-layout.component"; export * from "./fingerprint-dialog/fingerprint-dialog.component"; export * from "./password-callout/password-callout.component";