1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-26 14:23:46 +00:00

[CL-841] landing layout component (#17969)

* wip

* wip

* implement new control flow syntax

* new landing helper components

* add missing imports to header

* create max width container

* create landing card component

* address claude feedback

* fix center aligned text

* ensure secondary content is centered for now

* only render container when there is content

* remove max width container

* remove constructor init of variable

* remove unnecessary styling

* build styling into helper components

* ensure content grows

* ensure content takes allowed width

* apply padding to elements instead of header and check for projected content

* use Array.from to filter nodes

* fix logo width

* use has selector to apply actions styles, simplify heading padding

* remove unneeded content projection

* remove unneeded comment

* use modern control flow for story

* update max width classes to signal and remove switch

* make logo input readonly

* remove variables

* remove object type

* fix width types usage

* add comments about component usage

* fix broken variable reference

* fix broken max width class usage

* only appyly y padding to header actions
This commit is contained in:
Bryan Cunningham
2026-01-23 11:57:55 -05:00
committed by GitHub
parent 57378f1cb3
commit 6cf434cf11
19 changed files with 551 additions and 114 deletions

View File

@@ -7,10 +7,10 @@ import { Icon } from "@bitwarden/assets/svg";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Translation } from "../dialog";
import { LandingContentMaxWidthType } from "../landing-layout";
import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service";
import { AnonLayoutComponent, AnonLayoutMaxWidth } from "./anon-layout.component";
import { AnonLayoutComponent } from "./anon-layout.component";
export interface AnonLayoutWrapperData {
/**
* The optional title of the page.
@@ -35,7 +35,7 @@ export interface AnonLayoutWrapperData {
/**
* Optional flag to set the max-width of the page. Defaults to 'md' if not provided.
*/
maxWidth?: AnonLayoutMaxWidth;
maxWidth?: LandingContentMaxWidthType;
/**
* Hide the card that wraps the default content. Defaults to false.
*/
@@ -59,7 +59,7 @@ export class AnonLayoutWrapperComponent implements OnInit {
protected pageSubtitle?: string | null;
protected pageIcon: Icon | null = null;
protected showReadonlyHostname?: boolean | null;
protected maxWidth?: AnonLayoutMaxWidth | null;
protected maxWidth?: LandingContentMaxWidthType | null;
protected hideCardWrapper?: boolean | null;
protected hideBackgroundIllustration?: boolean | null;

View File

@@ -1,76 +1,26 @@
<main
class="tw-relative tw-flex tw-w-full tw-mx-auto tw-flex-col tw-bg-background-alt tw-p-5 tw-text-main"
[ngClass]="{
'tw-min-h-screen': clientType === 'web',
'tw-min-h-full': clientType === 'browser' || clientType === 'desktop',
}"
>
<div
[class]="
'tw-flex tw-justify-between tw-items-center tw-w-full' + (!hideLogo() ? ' tw-mb-12' : '')
"
>
@if (!hideLogo()) {
<a
[routerLink]="['/']"
class="tw-w-32 sm:tw-w-[200px] tw-self-center sm:tw-self-start tw-block [&>*]:tw-align-top"
>
<bit-icon [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
</a>
}
<div class="tw-ms-auto">
<ng-content select="[slot=header-actions]"></ng-content>
</div>
</div>
<bit-landing-layout [hideBackgroundIllustration]="hideBackgroundIllustration()">
<bit-landing-header [hideLogo]="hideLogo()">
<ng-content select="[slot=header-actions]"></ng-content>
</bit-landing-header>
<div class="tw-text-center tw-mb-4 sm:tw-mb-6 tw-mx-auto" [ngClass]="maxWidthClass">
@let iconInput = icon();
<!-- In some scenarios this icon's size is not limited by container width correctly -->
<!-- Targeting the SVG here to try and ensure it never grows too large in even the media queries are not working as expected -->
<div
*ngIf="iconInput !== null"
class="tw-size-20 sm:tw-size-24 [&_svg]:tw-w-full [&_svg]:tw-max-w-24 tw-mx-auto tw-content-center"
>
<bit-icon [icon]="iconInput"></bit-icon>
</div>
@if (title()) {
<!-- Small screens -->
<h1 bitTypography="h2" class="tw-mt-2 sm:tw-hidden">
{{ title() }}
</h1>
<!-- Medium to Larger screens -->
<h1 bitTypography="h1" class="tw-mt-2 tw-hidden sm:tw-block">
{{ title() }}
</h1>
}
@if (subtitle()) {
<div class="tw-text-sm sm:tw-text-base">{{ subtitle() }}</div>
}
</div>
<div
class="tw-z-10 tw-grow tw-w-full tw-mx-auto tw-flex tw-flex-col tw-items-center sm:tw-min-w-[28rem]"
[ngClass]="maxWidthClass"
>
<bit-landing-content [maxWidth]="maxWidth()">
<bit-landing-hero [icon]="icon()" [title]="title()" [subtitle]="subtitle()"></bit-landing-hero>
@if (hideCardWrapper()) {
<div class="tw-mb-6 sm:tw-mb-10">
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</div>
} @else {
<bit-base-card
class="!tw-rounded-2xl tw-mb-6 sm:tw-mb-10 tw-mx-auto tw-w-full tw-bg-transparent tw-border-none tw-shadow-none sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-100 sm:tw-shadow sm:tw-p-8"
>
<bit-landing-card>
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</bit-base-card>
</bit-landing-card>
}
<ng-content select="[slot=secondary]"></ng-content>
</div>
<div class="tw-flex tw-flex-col tw-items-center">
<ng-content select="[slot=secondary]"></ng-content>
</div>
</bit-landing-content>
@if (!hideFooter()) {
<footer class="tw-text-center tw-mt-4 sm:tw-mt-6">
<bit-landing-footer>
@if (showReadonlyHostname()) {
<div bitTypography="body2">{{ "accessing" | i18n }} {{ hostname }}</div>
} @else {
@@ -81,22 +31,9 @@
<div bitTypography="body2">&copy; {{ year }} Bitwarden Inc.</div>
<div bitTypography="body2">{{ version }}</div>
}
</footer>
</bit-landing-footer>
}
@if (!hideBackgroundIllustration()) {
<div
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-start-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
>
<bit-icon [icon]="leftIllustration"></bit-icon>
</div>
<div
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-end-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
>
<bit-icon [icon]="rightIllustration"></bit-icon>
</div>
}
</main>
</bit-landing-layout>
<ng-template #defaultContent>
<ng-content></ng-content>

View File

@@ -11,23 +11,17 @@ import {
import { RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
import {
BackgroundLeftIllustration,
BackgroundRightIllustration,
BitwardenLogo,
Icon,
} from "@bitwarden/assets/svg";
import { BitwardenLogo, Icon } from "@bitwarden/assets/svg";
import { ClientType } from "@bitwarden/common/enums";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { BaseCardComponent } from "../card";
import { IconModule } from "../icon";
import { LandingContentMaxWidthType } from "../landing-layout";
import { LandingLayoutModule } from "../landing-layout/landing-layout.module";
import { SharedModule } from "../shared";
import { TypographyModule } from "../typography";
export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
@@ -39,7 +33,7 @@ export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
TypographyModule,
SharedModule,
RouterModule,
BaseCardComponent,
LandingLayoutModule,
],
})
export class AnonLayoutComponent implements OnInit, OnChanges {
@@ -49,9 +43,6 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
return ["tw-h-full"];
}
readonly leftIllustration = BackgroundLeftIllustration;
readonly rightIllustration = BackgroundRightIllustration;
readonly title = input<string>();
readonly subtitle = input<string>();
readonly icon = model.required<Icon | null>();
@@ -66,7 +57,7 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
*
* @default 'md'
*/
readonly maxWidth = model<AnonLayoutMaxWidth>("md");
readonly maxWidth = model<LandingContentMaxWidthType>("md");
protected logo = BitwardenLogo;
protected year: string;
@@ -76,24 +67,6 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
protected hideYearAndVersion = false;
get maxWidthClass(): string {
const maxWidth = this.maxWidth();
switch (maxWidth) {
case "md":
return "tw-max-w-md";
case "lg":
return "tw-max-w-lg";
case "xl":
return "tw-max-w-xl";
case "2xl":
return "tw-max-w-2xl";
case "3xl":
return "tw-max-w-3xl";
case "4xl":
return "tw-max-w-4xl";
}
}
constructor(
private environmentService: EnvironmentService,
private platformUtilsService: PlatformUtilsService,

View File

@@ -25,6 +25,7 @@ export * from "./icon";
export * from "./icon-tile";
export * from "./input";
export * from "./item";
export * from "./landing-layout";
export * from "./layout";
export * from "./link";
export * from "./menu";

View File

@@ -0,0 +1,7 @@
export * from "./landing-layout.component";
export * from "./landing-layout.module";
export * from "./landing-card.component";
export * from "./landing-content.component";
export * from "./landing-footer.component";
export * from "./landing-header.component";
export * from "./landing-hero.component";

View File

@@ -0,0 +1,5 @@
<bit-base-card
class="tw-z-[2] tw-relative !tw-rounded-2xl tw-mb-6 sm:tw-mb-10 tw-mx-auto tw-w-full tw-bg-transparent tw-border-none tw-shadow-none sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-100 sm:tw-shadow sm:tw-p-8"
>
<ng-content></ng-content>
</bit-base-card>

View File

@@ -0,0 +1,33 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { BaseCardComponent } from "../card";
/**
* Card component for landing pages that wraps content in a styled container.
*
* @remarks
* This component provides:
* - Card-based layout with consistent styling
* - Content projection for forms, text, or other content
* - Proper elevation and border styling
*
* Use this component inside `bit-landing-content` to wrap forms, content sections,
* or any content that should appear in a contained, elevated card.
*
* @example
* ```html
* <bit-landing-card>
* <form>
* <!-- Your form fields here -->
* </form>
* </bit-landing-card>
* ```
*/
@Component({
selector: "bit-landing-card",
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [BaseCardComponent],
templateUrl: "./landing-card.component.html",
})
export class LandingCardComponent {}

View File

@@ -0,0 +1,8 @@
<div
class="tw-flex tw-flex-col tw-flex-1 tw-items-center tw-bg-background-alt tw-p-5 tw-pt-12 tw-text-main"
>
<div [class]="maxWidthClasses()">
<ng-content select="bit-landing-hero"></ng-content>
<ng-content></ng-content>
</div>
</div>

View File

@@ -0,0 +1,63 @@
import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core";
export const LandingContentMaxWidth = ["md", "lg", "xl", "2xl", "3xl", "4xl"] as const;
export type LandingContentMaxWidthType = (typeof LandingContentMaxWidth)[number];
/**
* Main content container for landing pages with configurable max-width constraints.
*
* @remarks
* This component provides:
* - Centered content area with alternative background color
* - Configurable maximum width to control content readability
* - Content projection slots for hero section and main content
* - Responsive padding and layout
*
* Use this component inside `bit-landing-layout` to wrap your main page content.
* Optionally include a `bit-landing-hero` as the first child for consistent hero section styling.
*
* @example
* ```html
* <bit-landing-content [maxWidth]="'xl'">
* <bit-landing-hero
* [icon]="lockIcon"
* [title]="'Welcome'"
* [subtitle]="'Get started with your account'"
* ></bit-landing-hero>
* <bit-landing-card>
* <!-- Your form or content here -->
* </bit-landing-card>
* </bit-landing-content>
* ```
*/
@Component({
selector: "bit-landing-content",
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: "./landing-content.component.html",
host: {
class: "tw-grow tw-flex tw-flex-col",
},
})
export class LandingContentComponent {
/**
* Max width of the landing layout container.
*
* @default "md"
*/
readonly maxWidth = input<LandingContentMaxWidthType>("md");
private readonly maxWidthClassMap: Record<LandingContentMaxWidthType, string> = {
md: "tw-max-w-md",
lg: "tw-max-w-lg",
xl: "tw-max-w-xl",
"2xl": "tw-max-w-2xl",
"3xl": "tw-max-w-3xl",
"4xl": "tw-max-w-4xl",
};
readonly maxWidthClasses = computed(() => {
const maxWidthClass = this.maxWidthClassMap[this.maxWidth()];
return `tw-flex tw-flex-col tw-w-full ${maxWidthClass}`;
});
}

View File

@@ -0,0 +1,3 @@
<footer class="tw-bg-background-alt tw-text-center tw-p-5 tw-pt-4 sm:tw-pt-6">
<ng-content></ng-content>
</footer>

View File

@@ -0,0 +1,29 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
/**
* Footer component for landing pages.
*
* @remarks
* This component provides:
* - Content projection for custom footer content (e.g., links, copyright, legal)
* - Consistent footer positioning at the bottom of the page
* - Proper z-index to appear above background illustrations
*
* Use this component inside `bit-landing-layout` as the last child to position it at the bottom.
*
* @example
* ```html
* <bit-landing-footer>
* <div class="tw-text-center tw-text-sm">
* <a routerLink="/privacy">Privacy</a>
* <span>© 2024 Bitwarden</span>
* </div>
* </bit-landing-footer>
* ```
*/
@Component({
selector: "bit-landing-footer",
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: "./landing-footer.component.html",
})
export class LandingFooterComponent {}

View File

@@ -0,0 +1,13 @@
<header class="tw-flex tw-w-full tw-bg-background-alt tw-px-5">
@if (!hideLogo()) {
<a
[routerLink]="['/']"
class="tw-w-32 tw-py-5 sm:tw-w-[200px] tw-self-center sm:tw-self-start tw-block [&>*]:tw-align-top"
>
<bit-icon [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
</a>
}
<div class="[&:has(*)]:tw-ms-auto [&:has(*)]:tw-py-5">
<ng-content></ng-content>
</div>
</header>

View File

@@ -0,0 +1,42 @@
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
import { RouterModule } from "@angular/router";
import { BitwardenLogo } from "@bitwarden/assets/svg";
import { IconModule } from "../icon";
import { SharedModule } from "../shared";
/**
* Header component for landing pages with optional Bitwarden logo and header actions slot.
*
* @remarks
* This component provides:
* - Optional Bitwarden logo with link to home page (left-aligned)
* - Default content projection slot for header actions (right-aligned, auto-margin left)
* - Consistent header styling across landing pages
* - Responsive layout that adapts logo size
*
* Use this component inside `bit-landing-layout` as the first child to position it at the top.
* Content projected into this component will automatically align to the right side of the header.
*
* @example
* ```html
* <bit-landing-header [hideLogo]="false">
* <!-- Content here appears in the right-aligned actions slot -->
* <nav>
* <a routerLink="/login">Log in</a>
* <button type="button">Sign up</button>
* </nav>
* </bit-landing-header>
* ```
*/
@Component({
selector: "bit-landing-header",
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: "./landing-header.component.html",
imports: [RouterModule, IconModule, SharedModule],
})
export class LandingHeaderComponent {
readonly hideLogo = input<boolean>(false);
protected readonly logo = BitwardenLogo;
}

View File

@@ -0,0 +1,28 @@
@if (icon() || title() || subtitle()) {
<div class="tw-text-center tw-mb-4 sm:tw-mb-6 tw-mx-auto">
@if (icon()) {
<!-- In some scenarios this icon's size is not limited by container width correctly -->
<!-- Targeting the SVG here to try and ensure it never grows too large in even the media queries are not working as expected -->
<div
class="tw-size-20 sm:tw-size-24 [&_svg]:tw-w-full [&_svg]:tw-max-w-24 tw-mx-auto tw-content-center"
>
<bit-icon [icon]="icon()"></bit-icon>
</div>
}
@if (title()) {
<!-- Small screens -->
<h1 bitTypography="h2" class="tw-mt-2 sm:tw-hidden">
{{ title() }}
</h1>
<!-- Medium to Larger screens -->
<h1 bitTypography="h1" class="tw-mt-2 tw-hidden sm:tw-block">
{{ title() }}
</h1>
}
@if (subtitle()) {
<div class="tw-text-sm sm:tw-text-base">{{ subtitle() }}</div>
}
</div>
}

View File

@@ -0,0 +1,40 @@
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
import { Icon } from "@bitwarden/assets/svg";
import { IconModule } from "../icon";
import { TypographyModule } from "../typography";
/**
* Hero section component for landing pages featuring an optional icon, title, and subtitle.
*
* @remarks
* This component provides:
* - Optional icon display (e.g., feature icons, status icons)
* - Large title text with consistent typography
* - Subtitle text for additional context
* - Centered layout with proper spacing
*
* Use this component as the first child inside `bit-landing-content` to create a prominent
* hero section that introduces the page's purpose.
*
* @example
* ```html
* <bit-landing-hero
* [icon]="lockIcon"
* [title]="'Secure Your Passwords'"
* [subtitle]="'Create your account to get started'"
* ></bit-landing-hero>
* ```
*/
@Component({
selector: "bit-landing-hero",
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: "./landing-hero.component.html",
imports: [IconModule, TypographyModule],
})
export class LandingHeroComponent {
readonly icon = input<Icon | null>(null);
readonly title = input<string | undefined>();
readonly subtitle = input<string | undefined>();
}

View File

@@ -0,0 +1,25 @@
<div
class="tw-relative tw-flex tw-size-full tw-mx-auto tw-flex-col"
[class]="{
'tw-min-h-screen': clientType === 'web',
'tw-min-h-full': clientType === 'browser' || clientType === 'desktop',
}"
>
<ng-content select="bit-landing-header"></ng-content>
<main class="tw-relative tw-flex tw-flex-1 tw-size-full tw-mx-auto tw-flex-col">
<ng-content></ng-content>
</main>
@if (!hideBackgroundIllustration()) {
<div
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-start-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
>
<bit-icon [icon]="leftIllustration"></bit-icon>
</div>
<div
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-end-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
>
<bit-icon [icon]="rightIllustration"></bit-icon>
</div>
}
<ng-content select="bit-landing-footer"></ng-content>
</div>

View File

@@ -0,0 +1,40 @@
import { Component, ChangeDetectionStrategy, inject, input } from "@angular/core";
import { BackgroundLeftIllustration, BackgroundRightIllustration } from "@bitwarden/assets/svg";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { IconModule } from "../icon";
/**
* Root layout component for landing pages providing a full-screen container with optional decorative background illustrations.
*
* @remarks
* This component serves as the outermost wrapper for landing pages and provides:
* - Full-screen layout that adapts to different client types (web, browser, desktop)
* - Optional decorative background illustrations in the bottom corners
* - Content projection slots for header, main content, and footer
*
* @example
* ```html
* <bit-landing-layout [hideBackgroundIllustration]="false">
* <bit-landing-header>...</bit-landing-header>
* <bit-landing-content>...</bit-landing-content>
* <bit-landing-footer>...</bit-landing-footer>
* </bit-landing-layout>
* ```
*/
@Component({
selector: "bit-landing-layout",
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: "./landing-layout.component.html",
imports: [IconModule],
})
export class LandingLayoutComponent {
readonly hideBackgroundIllustration = input<boolean>(false);
protected readonly leftIllustration = BackgroundLeftIllustration;
protected readonly rightIllustration = BackgroundRightIllustration;
private readonly platformUtilsService: PlatformUtilsService = inject(PlatformUtilsService);
protected readonly clientType = this.platformUtilsService.getClientType();
}

View File

@@ -0,0 +1,28 @@
import { NgModule } from "@angular/core";
import { LandingCardComponent } from "./landing-card.component";
import { LandingContentComponent } from "./landing-content.component";
import { LandingFooterComponent } from "./landing-footer.component";
import { LandingHeaderComponent } from "./landing-header.component";
import { LandingHeroComponent } from "./landing-hero.component";
import { LandingLayoutComponent } from "./landing-layout.component";
@NgModule({
imports: [
LandingLayoutComponent,
LandingHeaderComponent,
LandingHeroComponent,
LandingFooterComponent,
LandingContentComponent,
LandingCardComponent,
],
exports: [
LandingLayoutComponent,
LandingHeaderComponent,
LandingHeroComponent,
LandingFooterComponent,
LandingContentComponent,
LandingCardComponent,
],
})
export class LandingLayoutModule {}

View File

@@ -0,0 +1,162 @@
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { ClientType } from "@bitwarden/common/enums";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ButtonModule } from "../button";
import { LandingLayoutComponent } from "./landing-layout.component";
class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
getClientType = () => ClientType.Web;
}
type StoryArgs = LandingLayoutComponent & {
contentLength: "normal" | "long" | "thin";
includeHeader: boolean;
includeFooter: boolean;
};
export default {
title: "Component Library/Landing Layout",
component: LandingLayoutComponent,
decorators: [
moduleMetadata({
imports: [ButtonModule],
providers: [
{
provide: PlatformUtilsService,
useClass: MockPlatformUtilsService,
},
],
}),
],
render: (args) => {
return {
props: args,
template: /*html*/ `
<bit-landing-layout
[hideBackgroundIllustration]="hideBackgroundIllustration"
>
@if (includeHeader) {
<bit-landing-header>
<div class="tw-p-4">
<div class="tw-flex tw-items-center tw-gap-4">
<div class="tw-text-xl tw-font-semibold">Header Content</div>
</div>
</div>
</bit-landing-header>
}
<div>
@switch (contentLength) {
@case ('thin') {
<div class="tw-text-center tw-p-8">
<div class="tw-font-medium">Thin Content</div>
</div>
}
@case ('long') {
<div class="tw-p-8">
<div class="tw-font-medium tw-mb-4">Long Content</div>
<div class="tw-mb-4">Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</div>
<div class="tw-mb-4">Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</div>
</div>
}
@default {
<div class="tw-p-8">
<div class="tw-font-medium tw-mb-4">Normal Content</div>
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</div>
</div>
}
}
</div>
@if (includeFooter) {
<bit-landing-footer>
<div class="tw-text-center tw-text-sm tw-text-muted">
<div>Footer Content</div>
</div>
</bit-landing-footer>
}
</bit-landing-layout>
`,
};
},
argTypes: {
hideBackgroundIllustration: { control: "boolean" },
contentLength: {
control: "radio",
options: ["normal", "long", "thin"],
},
includeHeader: { control: "boolean" },
includeFooter: { control: "boolean" },
},
args: {
hideBackgroundIllustration: false,
contentLength: "normal",
includeHeader: false,
includeFooter: false,
},
} satisfies Meta<StoryArgs>;
type Story = StoryObj<StoryArgs>;
export const Default: Story = {
args: {
contentLength: "normal",
},
};
export const WithHeader: Story = {
args: {
includeHeader: true,
},
};
export const WithFooter: Story = {
args: {
includeFooter: true,
},
};
export const WithHeaderAndFooter: Story = {
args: {
includeHeader: true,
includeFooter: true,
},
};
export const LongContent: Story = {
args: {
contentLength: "long",
includeHeader: true,
includeFooter: true,
},
};
export const ThinContent: Story = {
args: {
contentLength: "thin",
includeHeader: true,
includeFooter: true,
},
};
export const NoBackgroundIllustration: Story = {
args: {
hideBackgroundIllustration: true,
includeHeader: true,
includeFooter: true,
},
};
export const MinimalState: Story = {
args: {
contentLength: "thin",
hideBackgroundIllustration: true,
includeHeader: false,
includeFooter: false,
},
};