mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
[PM-7343] AnonLayoutComponent Implementation Groundwork (#8585)
* test implementation * move files * adjust import and sample router comments * add storybook docs to anon-layout * rename to AnonLayoutWrapperComponent * update storybook docs * remove references to CL and replace with 'Auth-owned' * move AnonLayoutWrapperComponent to libs * add pageTitle input * add subTitle input * translate page title/subtitle, and refactor how icon is added * update tailwind.config and component styles * adjust spacing between primary and secondary content * move switch statement to wrapper * move icon to router file * update storybook documentation * fix storybook text color in normal code blocks * remove sample route * move wrapper component back to web * remove sample route * update storybook docs
This commit is contained in:
parent
af6a63c10b
commit
0fb352d8ed
@ -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)",
|
||||
|
4
apps/web/src/app/auth/anon-layout-wrapper.component.html
Normal file
4
apps/web/src/app/auth/anon-layout-wrapper.component.html
Normal file
@ -0,0 +1,4 @@
|
||||
<auth-anon-layout [title]="pageTitle" [subtitle]="pageSubtitle" [icon]="pageIcon">
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet slot="secondary" name="secondary"></router-outlet>
|
||||
</auth-anon-layout>
|
34
apps/web/src/app/auth/anon-layout-wrapper.component.ts
Normal file
34
apps/web/src/app/auth/anon-layout-wrapper.component.ts
Normal file
@ -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");
|
||||
}
|
||||
}
|
@ -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}",
|
||||
];
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<main
|
||||
class="tw-flex tw-min-h-screen tw-max-w-xl tw-w-full tw-mx-auto tw-flex-col tw-gap-9 tw-px-4 tw-pb-4 tw-pt-14 tw-text-main"
|
||||
class="tw-flex tw-min-h-screen tw-w-full tw-mx-auto tw-flex-col tw-gap-9 tw-px-4 tw-pb-4 tw-pt-14 tw-text-main"
|
||||
>
|
||||
<div class="tw-text-center">
|
||||
<div class="tw-px-8">
|
||||
@ -13,8 +13,10 @@
|
||||
</h1>
|
||||
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
|
||||
</div>
|
||||
<div class="tw-mb-auto tw-mx-auto tw-max-w-md tw-grid tw-gap-9">
|
||||
<div class="tw-rounded-xl sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8">
|
||||
<div class="tw-mb-auto tw-mx-auto tw-grid">
|
||||
<div
|
||||
class="tw-rounded-xl tw-mb-9 tw-mx-auto sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<ng-content select="[slot=secondary]"></ng-content>
|
||||
|
@ -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,
|
||||
|
118
libs/auth/src/angular/anon-layout/anon-layout.mdx
Normal file
118
libs/auth/src/angular/anon-layout/anon-layout.mdx
Normal file
@ -0,0 +1,118 @@
|
||||
import { Meta, Story, Controls } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./anon-layout.stories";
|
||||
|
||||
<Meta of={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
|
||||
<!-- File: example.component.html -->
|
||||
|
||||
<auth-anon-layout>
|
||||
<div>Example component content</div>
|
||||
</auth-anon-layout>
|
||||
```
|
||||
|
||||
### 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 - `<auth-anon-layout>`
|
||||
- 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
|
||||
<!-- File: anon-layout-wrapper.component.html -->
|
||||
|
||||
<auth-anon-layout>
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet slot="secondary" name="secondary"></router-outlet>
|
||||
</auth-anon-layout>
|
||||
```
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<Story of={stories.WithSecondaryContent} />
|
@ -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<PlatformUtilsService> {
|
||||
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 <div> ) and styling below is just a
|
||||
* sample and could be replaced with any content and styling
|
||||
*/
|
||||
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
|
||||
`
|
||||
<auth-anon-layout [title]="title" [subtitle]="subtitle">
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>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?</div>
|
||||
<div class="tw-max-w-md">
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>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?</div>
|
||||
</div>
|
||||
</auth-anon-layout>
|
||||
`,
|
||||
}),
|
||||
@ -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 <div>'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.
|
||||
`
|
||||
<auth-anon-layout [title]="title" [subtitle]="subtitle">
|
||||
<div>
|
||||
<div class="tw-max-w-md">
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>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?</div>
|
||||
</div>
|
||||
|
||||
<div slot="secondary" class="text-center">
|
||||
<div slot="secondary" class="text-center tw-max-w-md">
|
||||
<div class="tw-font-bold tw-mb-2">Secondary Projected Content (optional)</div>
|
||||
<button bitButton>Perform Action</button>
|
||||
</div>
|
||||
@ -75,14 +75,16 @@ export const WithSecondaryContent: Story = {
|
||||
export const WithLongContent: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template:
|
||||
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
|
||||
`
|
||||
<auth-anon-layout title="Page Title lorem ipsum dolor consectetur sit amet expedita quod est" subtitle="Subtitle here Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?">
|
||||
<div>
|
||||
<div class="tw-max-w-md">
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>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.</div>
|
||||
</div>
|
||||
|
||||
<div slot="secondary" class="text-center">
|
||||
<div slot="secondary" class="text-center tw-max-w-md">
|
||||
<div class="tw-font-bold tw-mb-2">Secondary Projected Content (optional)</div>
|
||||
<p>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? </p>
|
||||
<button bitButton>Perform Action</button>
|
||||
@ -95,9 +97,11 @@ export const WithLongContent: Story = {
|
||||
export const WithIcon: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template:
|
||||
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
|
||||
`
|
||||
<auth-anon-layout [title]="title" [subtitle]="subtitle" [icon]="icon">
|
||||
<div>
|
||||
<div class="tw-max-w-md">
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>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?</div>
|
||||
</div>
|
||||
|
@ -1 +1,3 @@
|
||||
export * from "./bitwarden-logo.icon";
|
||||
export * from "./lock.icon";
|
||||
export * from "./user-verification-biometrics-fingerprint.icon";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const IconLock = svgIcon`
|
||||
export const LockIcon = svgIcon`
|
||||
<svg width="65" height="80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class="tw-fill-primary-600" d="M36.554 52.684a4.133 4.133 0 0 0-.545-2.085 4.088 4.088 0 0 0-1.514-1.518 4.022 4.022 0 0 0-4.114.072 4.094 4.094 0 0 0-1.461 1.57 4.153 4.153 0 0 0 .175 4.16c.393.616.94 1.113 1.588 1.44v6.736a1.864 1.864 0 0 0 .498 1.365c.17.18.376.328.603.425a1.781 1.781 0 0 0 1.437 0c.227-.097.432-.242.603-.425a1.864 1.864 0 0 0 .499-1.365v-6.745a4.05 4.05 0 0 0 1.62-1.498c.392-.64.604-1.377.611-2.132ZM57.86 25.527h-2.242c-.175 0-.35-.037-.514-.105a1.3 1.3 0 0 1-.434-.297 1.379 1.379 0 0 1-.39-.963v-1a23 23 0 0 0-5.455-15.32A22.46 22.46 0 0 0 34.673.101a21.633 21.633 0 0 0-8.998 1.032 21.777 21.777 0 0 0-7.813 4.637 22.118 22.118 0 0 0-5.286 7.446 22.376 22.376 0 0 0-1.855 8.975v1.62c0 .03-.118 1.705-1.555 1.73h-2.02A6.723 6.723 0 0 0 2.37 27.56 6.887 6.887 0 0 0 .4 32.403V73.12a6.905 6.905 0 0 0 1.97 4.847A6.76 6.76 0 0 0 7.146 80h50.713a6.746 6.746 0 0 0 4.77-2.03 6.925 6.925 0 0 0 1.971-4.845V32.403a6.91 6.91 0 0 0-1.965-4.85 6.793 6.793 0 0 0-2.19-1.493 6.676 6.676 0 0 0-2.588-.53l.002-.003Zm-42.2-3.335c-.007-2.55.549-5.07 1.625-7.373a17.085 17.085 0 0 1 4.606-5.945 16.8 16.8 0 0 1 6.684-3.358 16.71 16.71 0 0 1 7.462-.115c3.835.91 7.245 3.12 9.665 6.266a17.61 17.61 0 0 1 3.64 11.02v1.475c0 .18-.035.358-.102.523a1.349 1.349 0 0 1-1.244.842H17.722a1.876 1.876 0 0 1-.744-.085 1.894 1.894 0 0 1-1.119-.957 1.98 1.98 0 0 1-.204-.728v-1.565h.005ZM59.663 73.12c0 .487-.19.952-.529 1.3a1.796 1.796 0 0 1-1.279.545H7.146a1.826 1.826 0 0 1-1.807-1.845V32.403a1.85 1.85 0 0 1 .523-1.3c.168-.17.365-.308.585-.4.22-.093.454-.14.691-.143h50.719c.479.005.938.2 1.276.545.339.345.526.81.526 1.295v40.717l.003.003Z" />
|
||||
</svg>
|
@ -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";
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user