1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00

Move to libs

This commit is contained in:
Hinton
2022-06-03 16:24:40 +02:00
parent 28d15bfe2a
commit d7492e3cf3
878 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
import { Component } from "@angular/core";
@Component({
selector: "app-root",
template: "",
})
export class AppComponent {
title = "components";
}

View File

@@ -0,0 +1,13 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, CommonModule],
providers: [{ provide: "WINDOW", useValue: window }],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@@ -0,0 +1,58 @@
import { Directive, ElementRef, HostBinding, Input } from "@angular/core";
type BadgeTypes = "primary" | "secondary" | "success" | "danger" | "warning" | "info";
const styles: Record<BadgeTypes, string[]> = {
primary: ["tw-bg-primary-500"],
secondary: ["tw-bg-text-muted"],
success: ["tw-bg-success-500"],
danger: ["tw-bg-danger-500"],
warning: ["tw-bg-warning-500"],
info: ["tw-bg-info-500"],
};
const hoverStyles: Record<BadgeTypes, string[]> = {
primary: ["hover:tw-bg-primary-700"],
secondary: ["hover:tw-bg-secondary-700"],
success: ["hover:tw-bg-success-700"],
danger: ["hover:tw-bg-danger-700"],
warning: ["hover:tw-bg-warning-700"],
info: ["hover:tw-bg-info-700"],
};
@Directive({
selector: "span[bitBadge], a[bitBadge], button[bitBadge]",
})
export class BadgeDirective {
@HostBinding("class") get classList() {
return [
"tw-inline-block",
"tw-py-1",
"tw-px-1.5",
"tw-font-bold",
"tw-leading-none",
"tw-text-center",
"!tw-text-contrast",
"tw-rounded",
"tw-border-none",
"tw-box-border",
"tw-whitespace-no-wrap",
"tw-text-xs",
"hover:tw-no-underline",
"focus:tw-outline-none",
"focus:tw-ring",
"focus:tw-ring-offset-2",
"focus:tw-ring-primary-700",
]
.concat(styles[this.badgeType])
.concat(this.hasHoverEffects ? hoverStyles[this.badgeType] : []);
}
@Input() badgeType: BadgeTypes = "primary";
private hasHoverEffects = false;
constructor(el: ElementRef<Element>) {
this.hasHoverEffects = el?.nativeElement?.nodeName != "SPAN";
}
}

View File

@@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { BadgeDirective } from "./badge.directive";
@NgModule({
imports: [CommonModule],
exports: [BadgeDirective],
declarations: [BadgeDirective],
})
export class BadgeModule {}

View File

@@ -0,0 +1,56 @@
import { Meta, Story } from "@storybook/angular";
import { BadgeDirective } from "./badge.directive";
export default {
title: "Jslib/Badge",
component: BadgeDirective,
args: {
badgeType: "primary",
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A16956",
},
},
} as Meta;
const Template: Story<BadgeDirective> = (args: BadgeDirective) => ({
props: args,
template: `
<span class="tw-text-main">Span </span><span bitBadge [badgeType]="badgeType">Badge</span>
<br><br>
<span class="tw-text-main">Link </span><a href="#" bitBadge [badgeType]="badgeType">Badge</a>
<br><br>
<span class="tw-text-main">Button </span><button bitBadge [badgeType]="badgeType">Badge</button>
`,
});
export const Primary = Template.bind({});
Primary.args = {};
export const Secondary = Template.bind({});
Secondary.args = {
badgeType: "secondary",
};
export const Success = Template.bind({});
Success.args = {
badgeType: "success",
};
export const Danger = Template.bind({});
Danger.args = {
badgeType: "danger",
};
export const Warning = Template.bind({});
Warning.args = {
badgeType: "warning",
};
export const Info = Template.bind({});
Info.args = {
badgeType: "info",
};

View File

@@ -0,0 +1,2 @@
export * from "./badge.directive";
export * from "./badge.module";

View File

@@ -0,0 +1,14 @@
<div
class="tw-py-2.5 tw-px-4 tw-text-contrast tw-flex tw-gap-2 tw-items-center"
[ngClass]="bannerClass"
[attr.role]="useAlertRole ? 'status' : null"
[attr.aria-live]="useAlertRole ? 'polite' : null"
>
<i class="bwi tw-align-middle" [ngClass]="icon" *ngIf="icon" aria-hidden="true"></i>
<span class="tw-text-base tw-grow">
<ng-content></ng-content>
</span>
<button class="tw-border-0 tw-bg-transparent tw-text-contrast tw-p-0" (click)="onClose.emit()">
<i class="bwi bwi-close tw-text-sm" *ngIf="icon" aria-hidden="true"></i>
</button>
</div>

View File

@@ -0,0 +1,35 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { BannerComponent } from "./banner.component";
describe("BannerComponent", () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BannerComponent],
}).compileComponents();
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create with alert", () => {
expect(component.useAlertRole).toBe(true);
const el = fixture.nativeElement.children[0];
expect(el.getAttribute("role")).toEqual("status");
expect(el.getAttribute("aria-live")).toEqual("polite");
});
it("useAlertRole=false", () => {
component.useAlertRole = false;
fixture.autoDetectChanges();
expect(component.useAlertRole).toBe(false);
const el = fixture.nativeElement.children[0];
expect(el.getAttribute("role")).toBeNull();
expect(el.getAttribute("aria-live")).toBeNull();
});
});

View File

@@ -0,0 +1,39 @@
import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core";
type BannerTypes = "premium" | "info" | "warning" | "danger";
const defaultIcon: Record<BannerTypes, string> = {
premium: "bwi-star",
info: "bwi-info-circle",
warning: "bwi-exclamation-triangle",
danger: "bwi-error",
};
@Component({
selector: "bit-banner",
templateUrl: "./banner.component.html",
})
export class BannerComponent implements OnInit {
@Input("bannerType") bannerType: BannerTypes = "info";
@Input() icon: string;
@Input() useAlertRole = true;
@Output() onClose = new EventEmitter<void>();
ngOnInit(): void {
this.icon ??= defaultIcon[this.bannerType];
}
get bannerClass() {
switch (this.bannerType) {
case "danger":
return "tw-bg-danger-500";
case "info":
return "tw-bg-info-500";
case "premium":
return "tw-bg-success-500";
case "warning":
return "tw-bg-warning-500";
}
}
}

View File

@@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { BannerComponent } from "./banner.component";
@NgModule({
imports: [CommonModule],
exports: [BannerComponent],
declarations: [BannerComponent],
})
export class BannerModule {}

View File

@@ -0,0 +1,38 @@
import { Meta, Story } from "@storybook/angular";
import { BannerComponent } from "./banner.component";
export default {
title: "Jslib/Banner",
component: BannerComponent,
args: {
bannerType: "warning",
},
} as Meta;
const Template: Story<BannerComponent> = (args: BannerComponent) => ({
props: args,
template: `
<bit-banner [bannerType]="bannerType">Content Really Long Text Lorem Ipsum Ipsum Ipsum <button>Button</button></bit-banner>
`,
});
export const Premium = Template.bind({});
Premium.args = {
bannerType: "premium",
};
export const Info = Template.bind({});
Info.args = {
bannerType: "info",
};
export const Warning = Template.bind({});
Warning.args = {
bannerType: "warning",
};
export const Danger = Template.bind({});
Danger.args = {
bannerType: "danger",
};

View File

@@ -0,0 +1,2 @@
export * from "./banner.component";
export * from "./banner.module";

View File

@@ -0,0 +1,77 @@
import { Component, DebugElement } from "@angular/core";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { ButtonModule } from "./index";
describe("Button", () => {
let fixture: ComponentFixture<TestApp>;
let testAppComponent: TestApp;
let buttonDebugElement: DebugElement;
let linkDebugElement: DebugElement;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [ButtonModule],
declarations: [TestApp],
});
TestBed.compileComponents();
fixture = TestBed.createComponent(TestApp);
testAppComponent = fixture.debugElement.componentInstance;
buttonDebugElement = fixture.debugElement.query(By.css("button"));
linkDebugElement = fixture.debugElement.query(By.css("a"));
})
);
it("should apply classes based on type", () => {
testAppComponent.buttonType = "primary";
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-bg-primary-500")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-bg-primary-500")).toBe(true);
testAppComponent.buttonType = "secondary";
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
testAppComponent.buttonType = "danger";
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true);
testAppComponent.buttonType = null;
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
});
it("should apply block when true and inline-block when false", () => {
testAppComponent.block = true;
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-block")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-block")).toBe(true);
expect(buttonDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(false);
expect(linkDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(false);
testAppComponent.block = false;
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(true);
expect(buttonDebugElement.nativeElement.classList.contains("tw-block")).toBe(false);
expect(linkDebugElement.nativeElement.classList.contains("tw-block")).toBe(false);
});
});
@Component({
selector: "test-app",
template: `
<button type="button" bitButton [buttonType]="buttonType" [block]="block">Button</button>
<a href="#" bitButton [buttonType]="buttonType" [block]="block"> Link </a>
`,
})
class TestApp {
buttonType: string;
block: boolean;
}

View File

@@ -0,0 +1,72 @@
import { Input, HostBinding, Directive } from "@angular/core";
export type ButtonTypes = "primary" | "secondary" | "danger";
const buttonStyles: Record<ButtonTypes, string[]> = {
primary: [
"tw-border-primary-500",
"tw-bg-primary-500",
"!tw-text-contrast",
"hover:tw-bg-primary-700",
"hover:tw-border-primary-700",
"focus:tw-bg-primary-700",
"focus:tw-border-primary-700",
],
secondary: [
"tw-bg-transparent",
"tw-border-text-muted",
"!tw-text-muted",
"hover:tw-bg-secondary-500",
"hover:tw-border-secondary-500",
"hover:!tw-text-contrast",
"focus:tw-bg-secondary-500",
"focus:tw-border-secondary-500",
"focus:!tw-text-contrast",
],
danger: [
"tw-bg-transparent",
"tw-border-danger-500",
"!tw-text-danger",
"hover:tw-bg-danger-500",
"hover:tw-border-danger-500",
"hover:!tw-text-contrast",
"focus:tw-bg-danger-500",
"focus:tw-border-danger-500",
"focus:!tw-text-contrast",
],
};
@Directive({
selector: "button[bitButton], a[bitButton]",
})
export class ButtonDirective {
@HostBinding("class") get classList() {
return [
"tw-font-semibold",
"tw-py-1.5",
"tw-px-3",
"tw-rounded",
"tw-transition",
"tw-border",
"tw-border-solid",
"tw-text-center",
"hover:tw-no-underline",
"disabled:tw-bg-secondary-100",
"disabled:tw-border-secondary-100",
"disabled:!tw-text-main",
"focus:tw-outline-none",
"focus:tw-ring",
"focus:tw-ring-offset-2",
"focus:tw-ring-primary-700",
"focus:tw-z-10",
]
.concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
.concat(buttonStyles[this.buttonType] ?? []);
}
@Input()
buttonType: ButtonTypes = "secondary";
@Input()
block = false;
}

View File

@@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { ButtonDirective } from "./button.directive";
@NgModule({
imports: [CommonModule],
exports: [ButtonDirective],
declarations: [ButtonDirective],
})
export class ButtonModule {}

View File

@@ -0,0 +1,54 @@
import { Meta, Story } from "@storybook/angular";
import { ButtonDirective } from "./button.directive";
export default {
title: "Jslib/Button",
component: ButtonDirective,
args: {
buttonType: "primary",
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A16733",
},
},
} as Meta;
const Template: Story<ButtonDirective> = (args: ButtonDirective) => ({
props: args,
template: `
<button bitButton [buttonType]="buttonType" [block]="block">Button</button>
<a bitButton [buttonType]="buttonType" [block]="block" href="#" class="tw-ml-2">Link</a>
`,
});
export const Primary = Template.bind({});
Primary.args = {
buttonType: "primary",
};
export const Secondary = Template.bind({});
Secondary.args = {
buttonType: "secondary",
};
export const Danger = Template.bind({});
Danger.args = {
buttonType: "danger",
};
const DisabledTemplate: Story = (args) => ({
props: args,
template: `
<button bitButton disabled buttonType="primary" class="tw-mr-2">Primary</button>
<button bitButton disabled buttonType="secondary" class="tw-mr-2">Secondary</button>
<button bitButton disabled buttonType="danger" class="tw-mr-2">Danger</button>
`,
});
export const Disabled = DisabledTemplate.bind({});
Disabled.args = {
size: "small",
};

View File

@@ -0,0 +1,2 @@
export * from "./button.directive";
export * from "./button.module";

View File

@@ -0,0 +1,14 @@
<div
class="tw-py-3 tw-px-5 tw-mb-4 tw-leading-5 tw-rounded tw-bg-background-alt tw-border tw-border-secondary-300 tw-border-solid tw-box-border tw-border-l-8 tw-text-main"
[ngClass]="calloutClass"
>
<h3
class="tw-mt-0 tw-mb-2 tw-text-base tw-font-bold tw-uppercase"
[ngClass]="headerClass"
*ngIf="title"
>
<i class="bwi {{ icon }}" *ngIf="icon" aria-hidden="true"></i>
{{ title }}
</h3>
<ng-content></ng-content>
</div>

View File

@@ -0,0 +1,64 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { I18nMockService } from "../utils/i18n-mock.service";
import { CalloutComponent } from ".";
describe("Callout", () => {
let component: CalloutComponent;
let fixture: ComponentFixture<CalloutComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [CalloutComponent],
providers: [
{
provide: I18nService,
useFactory: () =>
new I18nMockService({
warning: "Warning",
error: "Error",
}),
},
],
});
fixture = TestBed.createComponent(CalloutComponent);
component = fixture.componentInstance;
});
describe("default state", () => {
it("success", () => {
component.type = "success";
fixture.detectChanges();
expect(component.title).toBeUndefined();
expect(component.icon).toBe("bwi-check");
expect(component.headerClass).toBe("!tw-text-success");
});
it("info", () => {
component.type = "info";
fixture.detectChanges();
expect(component.title).toBeUndefined();
expect(component.icon).toBe("bwi-info-circle");
expect(component.headerClass).toBe("!tw-text-info");
});
it("warning", () => {
component.type = "warning";
fixture.detectChanges();
expect(component.title).toBe("Warning");
expect(component.icon).toBe("bwi-exclamation-triangle");
expect(component.headerClass).toBe("!tw-text-warning");
});
it("danger", () => {
component.type = "danger";
fixture.detectChanges();
expect(component.title).toBe("Error");
expect(component.icon).toBe("bwi-error");
expect(component.headerClass).toBe("!tw-text-danger");
});
});
});

View File

@@ -0,0 +1,63 @@
import { Component, Input, OnInit } from "@angular/core";
import { I18nService } from "jslib-common/abstractions/i18n.service";
type CalloutTypes = "success" | "info" | "warning" | "danger";
const defaultIcon: Record<CalloutTypes, string> = {
success: "bwi-check",
info: "bwi-info-circle",
warning: "bwi-exclamation-triangle",
danger: "bwi-error",
};
const defaultI18n: Partial<Record<CalloutTypes, string>> = {
warning: "warning",
danger: "error",
};
@Component({
selector: "bit-callout",
templateUrl: "callout.component.html",
})
export class CalloutComponent implements OnInit {
@Input() type: CalloutTypes = "info";
@Input() icon: string;
@Input() title: string;
@Input() useAlertRole = false;
constructor(private i18nService: I18nService) {}
ngOnInit() {
this.icon ??= defaultIcon[this.type];
if (this.title == null && defaultI18n[this.type] != null) {
this.title = this.i18nService.t(defaultI18n[this.type]);
}
}
get calloutClass() {
switch (this.type) {
case "danger":
return "tw-border-l-danger-500";
case "info":
return "tw-border-l-info-500";
case "success":
return "tw-border-l-success-500";
case "warning":
return "tw-border-l-warning-500";
}
}
get headerClass() {
switch (this.type) {
case "danger":
return "!tw-text-danger";
case "info":
return "!tw-text-info";
case "success":
return "!tw-text-success";
case "warning":
return "!tw-text-warning";
}
}
}

View File

@@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { CalloutComponent } from "./callout.component";
@NgModule({
imports: [CommonModule],
exports: [CalloutComponent],
declarations: [CalloutComponent],
})
export class CalloutModule {}

View File

@@ -0,0 +1,65 @@
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { I18nMockService } from "../utils/i18n-mock.service";
import { CalloutComponent } from "./callout.component";
export default {
title: "Jslib/Callout",
component: CalloutComponent,
decorators: [
moduleMetadata({
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
warning: "Warning",
error: "Error",
});
},
},
],
}),
],
args: {
type: "warning",
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17484",
},
},
} as Meta;
const Template: Story<CalloutComponent> = (args: CalloutComponent) => ({
props: args,
template: `
<bit-callout [type]="type" [title]="title">Content</bit-callout>
`,
});
export const Success = Template.bind({});
Success.args = {
type: "success",
title: "Success",
};
export const Info = Template.bind({});
Info.args = {
type: "info",
title: "Info",
};
export const Warning = Template.bind({});
Warning.args = {
type: "warning",
};
export const Danger = Template.bind({});
Danger.args = {
type: "danger",
};

View File

@@ -0,0 +1,2 @@
export * from "./callout.module";
export * from "./callout.component";

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View File

@@ -0,0 +1,39 @@
import { Component, Input } from "@angular/core";
import { AbstractControl, FormGroup } from "@angular/forms";
@Component({
selector: "bit-error-summary",
template: ` <ng-container *ngIf="errorCount > 0">
<i class="bwi bwi-error"></i> {{ "fieldsNeedAttention" | i18n: errorString }}
</ng-container>`,
host: {
class: "tw-block tw-text-danger tw-mt-2",
"aria-live": "assertive",
},
})
export class BitErrorSummary {
@Input()
formGroup: FormGroup;
get errorCount(): number {
return this.getErrorCount(this.formGroup);
}
get errorString() {
return this.errorCount.toString();
}
private getErrorCount(form: FormGroup): number {
return Object.values(form.controls).reduce((acc: number, control: AbstractControl) => {
if (control instanceof FormGroup) {
return acc + this.getErrorCount(control);
}
if (control.errors == null) {
return acc;
}
return acc + Object.keys(control.errors).length;
}, 0);
}
}

View File

@@ -0,0 +1,78 @@
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { InputModule } from "src/input/input.module";
import { I18nMockService } from "src/utils/i18n-mock.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { ButtonModule } from "../button";
import { BitFormFieldComponent } from "./form-field.component";
import { FormFieldModule } from "./form-field.module";
export default {
title: "Jslib/Form Error Summary",
component: BitFormFieldComponent,
decorators: [
moduleMetadata({
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
required: "required",
inputRequired: "Input is required.",
inputEmail: "Input is not an email-address.",
fieldsNeedAttention: "$COUNT$ field(s) above need your attention.",
});
},
},
],
}),
],
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689",
},
},
} as Meta;
const fb = new FormBuilder();
const formObj = fb.group({
name: ["", [Validators.required]],
email: ["", [Validators.required, Validators.email]],
});
function submit() {
formObj.markAllAsTouched();
}
const Template: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: {
formObj: formObj,
submit: submit,
...args,
},
template: `
<form [formGroup]="formObj" (ngSubmit)="submit()">
<bit-form-field>
<bit-label>Name</bit-label>
<input bitInput formControlName="name" />
</bit-form-field>
<bit-form-field>
<bit-label>Email</bit-label>
<input bitInput formControlName="email" />
</bit-form-field>
<button type="submit" bitButton buttonType="primary">Submit</button>
<bit-error-summary [formGroup]="formObj"></bit-error-summary>
</form>
`,
});
export const Default = Template.bind({});
Default.props = {};

View File

@@ -0,0 +1,38 @@
import { Component, HostBinding, Input } from "@angular/core";
import { I18nService } from "jslib-common/abstractions/i18n.service";
// Increments for each instance of this component
let nextId = 0;
@Component({
selector: "bit-error",
template: `<i class="bwi bwi-error"></i> {{ displayError }}`,
host: {
class: "tw-block tw-mt-1 tw-text-danger",
"aria-live": "assertive",
},
})
export class BitErrorComponent {
@HostBinding() id = `bit-error-${nextId++}`;
@Input() error: [string, any];
constructor(private i18nService: I18nService) {}
get displayError() {
switch (this.error[0]) {
case "required":
return this.i18nService.t("inputRequired");
case "email":
return this.i18nService.t("inputEmail");
default:
// Attempt to show a custom error message.
if (this.error[1]?.message) {
return this.error[1]?.message;
}
return this.error;
}
}
}

View File

@@ -0,0 +1,17 @@
<label class="tw-block tw-font-semibold tw-mb-1 tw-text-main" [attr.for]="input.id">
<ng-content select="bit-label"></ng-content>
<span *ngIf="input.required" class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
</label>
<div class="tw-flex">
<div *ngIf="prefixChildren.length" class="tw-flex">
<ng-content select="[bitPrefix]"></ng-content>
</div>
<ng-content></ng-content>
<div *ngIf="prefixChildren.length" class="tw-flex">
<ng-content select="[bitSuffix]"></ng-content>
</div>
</div>
<ng-container [ngSwitch]="input.hasError">
<ng-content select="bit-hint" *ngSwitchCase="false"></ng-content>
<bit-error [error]="input.error" *ngSwitchCase="true"></bit-error>
</ng-container>

View File

@@ -0,0 +1,53 @@
import {
AfterContentChecked,
Component,
ContentChild,
ContentChildren,
QueryList,
ViewChild,
} from "@angular/core";
import { BitInputDirective } from "../input/input.directive";
import { BitErrorComponent } from "./error.component";
import { BitHintComponent } from "./hint.component";
import { BitPrefixDirective } from "./prefix.directive";
import { BitSuffixDirective } from "./suffix.directive";
@Component({
selector: "bit-form-field",
templateUrl: "./form-field.component.html",
host: {
class: "tw-mb-6 tw-block",
},
})
export class BitFormFieldComponent implements AfterContentChecked {
@ContentChild(BitInputDirective) input: BitInputDirective;
@ContentChild(BitHintComponent) hint: BitHintComponent;
@ViewChild(BitErrorComponent) error: BitErrorComponent;
@ContentChildren(BitPrefixDirective) prefixChildren: QueryList<BitPrefixDirective>;
@ContentChildren(BitSuffixDirective) suffixChildren: QueryList<BitSuffixDirective>;
ngAfterContentChecked(): void {
this.input.hasPrefix = this.prefixChildren.length > 0;
this.input.hasSuffix = this.suffixChildren.length > 0;
this.prefixChildren.forEach((prefix) => {
prefix.first = prefix == this.prefixChildren.first;
});
this.suffixChildren.forEach((suffix) => {
suffix.last = suffix == this.suffixChildren.last;
});
if (this.error) {
this.input.ariaDescribedBy = this.error.id;
} else if (this.hint) {
this.input.ariaDescribedBy = this.hint.id;
} else {
this.input.ariaDescribedBy = undefined;
}
}
}

View File

@@ -0,0 +1,54 @@
import { CommonModule } from "@angular/common";
import { NgModule, Pipe, PipeTransform } from "@angular/core";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { BitInputDirective } from "../input/input.directive";
import { InputModule } from "../input/input.module";
import { BitErrorSummary } from "./error-summary.component";
import { BitErrorComponent } from "./error.component";
import { BitFormFieldComponent } from "./form-field.component";
import { BitHintComponent } from "./hint.component";
import { BitLabel } from "./label.directive";
import { BitPrefixDirective } from "./prefix.directive";
import { BitSuffixDirective } from "./suffix.directive";
/**
* Temporarily duplicate this pipe
*/
@Pipe({
name: "i18n",
})
export class I18nPipe implements PipeTransform {
constructor(private i18nService: I18nService) {}
transform(id: string, p1?: string, p2?: string, p3?: string): string {
return this.i18nService.t(id, p1, p2, p3);
}
}
@NgModule({
imports: [CommonModule, InputModule],
exports: [
BitErrorComponent,
BitErrorSummary,
BitFormFieldComponent,
BitHintComponent,
BitInputDirective,
BitLabel,
BitPrefixDirective,
BitSuffixDirective,
],
declarations: [
BitErrorComponent,
BitErrorSummary,
BitFormFieldComponent,
BitHintComponent,
BitLabel,
BitPrefixDirective,
BitSuffixDirective,
I18nPipe,
],
})
export class FormFieldModule {}

View File

@@ -0,0 +1,212 @@
import {
AbstractControl,
FormBuilder,
FormsModule,
ReactiveFormsModule,
ValidationErrors,
ValidatorFn,
Validators,
} from "@angular/forms";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { InputModule } from "src/input/input.module";
import { I18nMockService } from "src/utils/i18n-mock.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { ButtonModule } from "../button";
import { BitFormFieldComponent } from "./form-field.component";
import { FormFieldModule } from "./form-field.module";
export default {
title: "Jslib/Form Field",
component: BitFormFieldComponent,
decorators: [
moduleMetadata({
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
required: "required",
inputRequired: "Input is required.",
inputEmail: "Input is not an email-address.",
});
},
},
],
}),
],
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689",
},
},
} as Meta;
const fb = new FormBuilder();
const formObj = fb.group({
test: [""],
required: ["", [Validators.required]],
});
const defaultFormObj = fb.group({
name: ["", [Validators.required]],
email: ["", [Validators.required, Validators.email, forbiddenNameValidator(/bit/i)]],
});
// Custom error message, `message` is shown as the error message
function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = nameRe.test(control.value);
return forbidden ? { forbiddenName: { message: "forbiddenName" } } : null;
};
}
function submit() {
defaultFormObj.markAllAsTouched();
}
const Template: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: {
formObj: defaultFormObj,
submit: submit,
...args,
},
template: `
<form [formGroup]="formObj" (ngSubmit)="submit()">
<bit-form-field>
<bit-label>Name</bit-label>
<input bitInput formControlName="name" />
</bit-form-field>
<bit-form-field>
<bit-label>Email</bit-label>
<input bitInput formControlName="email" />
</bit-form-field>
<button type="submit" bitButton buttonType="primary">Submit</button>
</form>
`,
});
export const Default = Template.bind({});
Default.props = {};
const RequiredTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: {
formObj: formObj,
...args,
},
template: `
<bit-form-field>
<bit-label>Label</bit-label>
<input bitInput required placeholder="Placeholder" />
</bit-form-field>
<bit-form-field [formGroup]="formObj">
<bit-label>FormControl</bit-label>
<input bitInput formControlName="required" placeholder="Placeholder" />
</bit-form-field>
`,
});
export const Required = RequiredTemplate.bind({});
Required.props = {};
const HintTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: {
formObj: formObj,
...args,
},
template: `
<bit-form-field [formGroup]="formObj">
<bit-label>FormControl</bit-label>
<input bitInput formControlName="required" placeholder="Placeholder" />
<bit-hint>Long hint text</bit-hint>
</bit-form-field>
`,
});
export const Hint = HintTemplate.bind({});
Required.props = {};
const DisabledTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: args,
template: `
<bit-form-field>
<bit-label>Label</bit-label>
<input bitInput placeholder="Placeholder" disabled />
</bit-form-field>
`,
});
export const Disabled = DisabledTemplate.bind({});
Disabled.args = {};
const GroupTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: args,
template: `
<bit-form-field>
<bit-label>Label</bit-label>
<input bitInput placeholder="Placeholder" />
<span bitPrefix>$</span>
<span bitSuffix>USD</span>
</bit-form-field>
`,
});
export const InputGroup = GroupTemplate.bind({});
InputGroup.args = {};
const ButtonGroupTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: args,
template: `
<bit-form-field>
<bit-label>Label</bit-label>
<input bitInput placeholder="Placeholder" />
<button bitPrefix bitButton>Button</button>
<button bitPrefix bitButton>Button</button>
<button bitSuffix bitButton>
<i aria-hidden="true" class="bwi bwi-lg bwi-eye"></i>
</button>
<button bitSuffix bitButton>
<i aria-hidden="true" class="bwi bwi-lg bwi-clone"></i>
</button>
</bit-form-field>
`,
});
export const ButtonInputGroup = ButtonGroupTemplate.bind({});
ButtonInputGroup.args = {};
const SelectTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: args,
template: `
<bit-form-field>
<bit-label>Label</bit-label>
<select bitInput>
<option>Select</option>
<option>Other</option>
</select>
</bit-form-field>
`,
});
export const Select = SelectTemplate.bind({});
Select.args = {};
const TextareaTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: args,
template: `
<bit-form-field>
<bit-label>Textarea</bit-label>
<textarea bitInput rows="4"></textarea>
</bit-form-field>
`,
});
export const Textarea = TextareaTemplate.bind({});
Textarea.args = {};

View File

@@ -0,0 +1,14 @@
import { Directive, HostBinding } from "@angular/core";
// Increments for each instance of this component
let nextId = 0;
@Directive({
selector: "bit-hint",
host: {
class: "tw-text-muted tw-inline-block tw-mt-1",
},
})
export class BitHintComponent {
@HostBinding() id = `bit-hint-${nextId++}`;
}

View File

@@ -0,0 +1,2 @@
export * from "./form-field.module";
export * from "./form-field.component";

View File

@@ -0,0 +1,6 @@
import { Directive } from "@angular/core";
@Directive({
selector: "bit-label",
})
export class BitLabel {}

View File

@@ -0,0 +1,28 @@
import { Directive, HostBinding, Input } from "@angular/core";
export const PrefixClasses = [
"tw-block",
"tw-px-3",
"tw-py-1.5",
"tw-bg-background-alt",
"tw-border",
"tw-border-solid",
"tw-border-secondary-500",
"tw-text-muted",
"tw-rounded",
];
@Directive({
selector: "[bitPrefix]",
})
export class BitPrefixDirective {
@HostBinding("class") @Input() get classList() {
return PrefixClasses.concat([
"tw-border-r-0",
"tw-rounded-r-none",
!this.first ? "tw-rounded-l-none" : "",
]).filter((c) => c != "");
}
@Input() first = false;
}

View File

@@ -0,0 +1,18 @@
import { Directive, HostBinding, Input } from "@angular/core";
import { PrefixClasses } from "./prefix.directive";
@Directive({
selector: "[bitSuffix]",
})
export class BitSuffixDirective {
@HostBinding("class") @Input() get classList() {
return PrefixClasses.concat([
"tw-rounded-l-none",
"tw-border-l-0",
!this.last ? "tw-rounded-r-none" : "",
]).filter((c) => c != "");
}
@Input() last = false;
}

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Components</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,6 @@
export * from "./badge";
export * from "./banner";
export * from "./button";
export * from "./callout";
export * from "./form-field";
export * from "./menu";

View File

@@ -0,0 +1,65 @@
import { Directive, HostBinding, Input, Optional, Self } from "@angular/core";
import { NgControl, Validators } from "@angular/forms";
// Increments for each instance of this component
let nextId = 0;
@Directive({
selector: "input[bitInput], select[bitInput], textarea[bitInput]",
})
export class BitInputDirective {
@HostBinding("class") @Input() get classList() {
return [
"tw-block",
"tw-w-full",
"tw-px-3",
"tw-py-1.5",
"tw-bg-background-alt",
"tw-border",
"tw-border-solid",
"tw-rounded",
"tw-text-main",
"tw-placeholder-text-muted",
"focus:tw-outline-none",
"focus:tw-border-primary-700",
"focus:tw-ring-1",
"focus:tw-ring-primary-700",
"focus:tw-z-10",
"disabled:tw-bg-secondary-100",
this.hasPrefix ? "tw-rounded-l-none" : "",
this.hasSuffix ? "tw-rounded-r-none" : "",
this.hasError ? "tw-border-danger-500" : "tw-border-secondary-500",
].filter((s) => s != "");
}
@HostBinding() id = `bit-input-${nextId++}`;
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
@HostBinding("attr.aria-invalid") get ariaInvalid() {
return this.hasError ? true : undefined;
}
@HostBinding()
@Input()
get required() {
return this._required ?? this.ngControl?.control?.hasValidator(Validators.required) ?? false;
}
set required(value: any) {
this._required = value != null && value !== false;
}
private _required: boolean;
@Input() hasPrefix = false;
@Input() hasSuffix = false;
get hasError() {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
}
get error(): [string, any] {
const key = Object.keys(this.ngControl.errors)[0];
return [key, this.ngControl.errors[key]];
}
constructor(@Optional() @Self() private ngControl: NgControl) {}
}

View File

@@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { BitInputDirective } from "./input.directive";
@NgModule({
imports: [CommonModule],
declarations: [BitInputDirective],
exports: [BitInputDirective],
})
export class InputModule {}

View File

@@ -0,0 +1,7 @@
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err)); // eslint-disable-line

View File

@@ -0,0 +1,5 @@
export * from "./menu.module";
export * from "./menu.component";
export * from "./menu-trigger-for.directive";
export * from "./menu-item.directive";
export * from "./menu-divider.component";

View File

@@ -0,0 +1,4 @@
<div
class="tw-border-solid tw-border-0 tw-border-t tw-border-t-secondary-500 tw-my-2"
role="separator"
></div>

View File

@@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "bit-menu-divider",
templateUrl: "./menu-divider.component.html",
})
export class MenuDividerComponent {}

View File

@@ -0,0 +1,36 @@
import { FocusableOption } from "@angular/cdk/a11y";
import { Directive, ElementRef, HostBinding } from "@angular/core";
@Directive({
selector: "[bitMenuItem]",
})
export class MenuItemDirective implements FocusableOption {
@HostBinding("class") classList = [
"tw-block",
"tw-py-1",
"tw-px-4",
"!tw-text-main",
"!tw-no-underline",
"tw-cursor-pointer",
"tw-border-none",
"tw-bg-background",
"tw-text-left",
"hover:tw-bg-secondary-100",
"focus:tw-bg-secondary-100",
"focus:tw-z-50",
"focus:tw-outline-none",
"focus:tw-ring",
"focus:tw-ring-offset-2",
"focus:tw-ring-primary-700",
"active:!tw-ring-0",
"active:!tw-ring-offset-0",
];
@HostBinding("attr.role") role = "menuitem";
@HostBinding("tabIndex") tabIndex = "-1";
constructor(private elementRef: ElementRef) {}
focus() {
this.elementRef.nativeElement.focus();
}
}

View File

@@ -0,0 +1,119 @@
import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal";
import {
Directive,
ElementRef,
HostBinding,
HostListener,
Input,
OnDestroy,
ViewContainerRef,
} from "@angular/core";
import { Observable, Subscription } from "rxjs";
import { filter, mergeWith } from "rxjs/operators";
import { MenuComponent } from "./menu.component";
@Directive({
selector: "[bitMenuTriggerFor]",
})
export class MenuTriggerForDirective implements OnDestroy {
@HostBinding("attr.aria-expanded") isOpen = false;
@HostBinding("attr.aria-haspopup") hasPopup = "menu";
@HostBinding("attr.role") role = "button";
@Input("bitMenuTriggerFor") menu: MenuComponent;
private overlayRef: OverlayRef;
private defaultMenuConfig: OverlayConfig = {
panelClass: "bit-menu-panel",
hasBackdrop: true,
backdropClass: "cdk-overlay-transparent-backdrop",
scrollStrategy: this.overlay.scrollStrategies.reposition(),
positionStrategy: this.overlay
.position()
.flexibleConnectedTo(this.elementRef)
.withPositions([
{
originX: "start",
originY: "bottom",
overlayX: "start",
overlayY: "top",
},
{
originX: "end",
originY: "bottom",
overlayX: "end",
overlayY: "top",
},
])
.withLockedPosition(true)
.withFlexibleDimensions(false)
.withPush(false),
};
private closedEventsSub: Subscription;
private keyDownEventsSub: Subscription;
constructor(
private elementRef: ElementRef<HTMLElement>,
private viewContainerRef: ViewContainerRef,
private overlay: Overlay
) {}
@HostListener("click") toggleMenu() {
this.isOpen ? this.destroyMenu() : this.openMenu();
}
ngOnDestroy() {
this.disposeAll();
}
private openMenu() {
if (this.menu == null) {
throw new Error("Cannot find bit-menu element");
}
this.isOpen = true;
this.overlayRef = this.overlay.create(this.defaultMenuConfig);
const templatePortal = new TemplatePortal(this.menu.templateRef, this.viewContainerRef);
this.overlayRef.attach(templatePortal);
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
if (event?.key === "Tab") {
// Required to ensure tab order resumes correctly
this.elementRef.nativeElement.focus();
}
this.destroyMenu();
});
this.keyDownEventsSub = this.overlayRef
.keydownEvents()
.subscribe((event: KeyboardEvent) => this.menu.keyManager.onKeydown(event));
}
private destroyMenu() {
if (this.overlayRef == null || !this.isOpen) {
return;
}
this.isOpen = false;
this.disposeAll();
}
private getClosedEvents(): Observable<any> {
const detachments = this.overlayRef.detachments();
const escKey = this.overlayRef
.keydownEvents()
.pipe(filter((event: KeyboardEvent) => event.key === "Escape" || event.key === "Tab"));
const backdrop = this.overlayRef.backdropClick();
const menuClosed = this.menu.closed;
return detachments.pipe(mergeWith(escKey, backdrop, menuClosed));
}
private disposeAll() {
this.closedEventsSub?.unsubscribe();
this.overlayRef?.dispose();
this.keyDownEventsSub?.unsubscribe();
}
}

View File

@@ -0,0 +1,9 @@
<ng-template>
<div
(click)="closed.emit()"
class="tw-flex tw-flex-col tw-bg-background tw-border tw-border-solid tw-rounded tw-border-secondary-500 tw-bg-clip-padding tw-py-2 tw-shrink-0"
role="menu"
>
<ng-content></ng-content>
</div>
</ng-template>

View File

@@ -0,0 +1,77 @@
import { Component } from "@angular/core";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { MenuTriggerForDirective } from "./menu-trigger-for.directive";
import { MenuModule } from "./index";
describe("Menu", () => {
let fixture: ComponentFixture<TestApp>;
const getMenuTriggerDirective = () => {
const buttonDebugElement = fixture.debugElement.query(By.directive(MenuTriggerForDirective));
return buttonDebugElement.injector.get(MenuTriggerForDirective);
};
// The overlay is created outside the root debugElement, so we need to query its parent
const getBitMenuPanel = () => document.querySelector(".bit-menu-panel");
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MenuModule],
declarations: [TestApp],
});
TestBed.compileComponents();
fixture = TestBed.createComponent(TestApp);
fixture.detectChanges();
})
);
it("should open when the trigger is clicked", async () => {
const buttonDebugElement = fixture.debugElement.query(By.directive(MenuTriggerForDirective));
(buttonDebugElement.nativeElement as HTMLButtonElement).click();
expect(getBitMenuPanel()).toBeTruthy();
});
it("should close when the trigger is clicked", () => {
getMenuTriggerDirective().toggleMenu();
const buttonDebugElement = fixture.debugElement.query(By.directive(MenuTriggerForDirective));
(buttonDebugElement.nativeElement as HTMLButtonElement).click();
expect(getBitMenuPanel()).toBeFalsy();
});
it("should close when a menu item is clicked", () => {
getMenuTriggerDirective().toggleMenu();
(document.querySelector("#item1") as HTMLAnchorElement).click();
expect(getBitMenuPanel()).toBeFalsy();
});
it("should close when the backdrop is clicked", () => {
getMenuTriggerDirective().toggleMenu();
(document.querySelector(".cdk-overlay-backdrop") as HTMLAnchorElement).click();
expect(getBitMenuPanel()).toBeFalsy();
});
});
@Component({
selector: "test-app",
template: `
<button type="button" [bitMenuTriggerFor]="testMenu" class="testclass">Open menu</button>
<bit-menu #testMenu>
<a id="item1" bitMenuItem>Item 1</a>
<a id="item2" bitMenuItem>Item 2</a>
</bit-menu>
`,
})
class TestApp {}

View File

@@ -0,0 +1,30 @@
import { FocusKeyManager } from "@angular/cdk/a11y";
import {
Component,
Output,
TemplateRef,
ViewChild,
EventEmitter,
ContentChildren,
QueryList,
AfterContentInit,
} from "@angular/core";
import { MenuItemDirective } from "./menu-item.directive";
@Component({
selector: "bit-menu",
templateUrl: "./menu.component.html",
exportAs: "menuComponent",
})
export class MenuComponent implements AfterContentInit {
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
@Output() closed = new EventEmitter<void>();
@ContentChildren(MenuItemDirective, { descendants: true })
menuItems: QueryList<MenuItemDirective>;
keyManager: FocusKeyManager<MenuItemDirective>;
ngAfterContentInit() {
this.keyManager = new FocusKeyManager(this.menuItems).withWrap();
}
}

View File

@@ -0,0 +1,15 @@
import { OverlayModule } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { MenuDividerComponent } from "./menu-divider.component";
import { MenuItemDirective } from "./menu-item.directive";
import { MenuTriggerForDirective } from "./menu-trigger-for.directive";
import { MenuComponent } from "./menu.component";
@NgModule({
imports: [CommonModule, OverlayModule],
declarations: [MenuComponent, MenuTriggerForDirective, MenuItemDirective, MenuDividerComponent],
exports: [MenuComponent, MenuTriggerForDirective, MenuItemDirective, MenuDividerComponent],
})
export class MenuModule {}

View File

@@ -0,0 +1,69 @@
import { OverlayModule } from "@angular/cdk/overlay";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { ButtonModule } from "../button/button.module";
import { MenuDividerComponent } from "./menu-divider.component";
import { MenuItemDirective } from "./menu-item.directive";
import { MenuTriggerForDirective } from "./menu-trigger-for.directive";
import { MenuComponent } from "./menu.component";
export default {
title: "Jslib/Menu",
component: MenuTriggerForDirective,
decorators: [
moduleMetadata({
declarations: [
MenuTriggerForDirective,
MenuComponent,
MenuItemDirective,
MenuDividerComponent,
],
imports: [OverlayModule, ButtonModule],
}),
],
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17952",
},
},
} as Meta;
const Template: Story<MenuTriggerForDirective> = (args: MenuTriggerForDirective) => ({
props: args,
template: `
<bit-menu #myMenu="menuComponent">
<a href="#" bitMenuItem>Anchor link</a>
<a href="#" bitMenuItem>Another link</a>
<button type="button" bitMenuItem>Button</button>
<bit-menu-divider></bit-menu-divider>
<button type="button" bitMenuItem>Button after divider</button>
</bit-menu>
<div class="tw-h-40">
<div class="cdk-overlay-pane bit-menu-panel">
<ng-container *ngTemplateOutlet="myMenu.templateRef"></ng-container>
</div>
</div>
`,
});
const TemplateWithButton: Story<MenuTriggerForDirective> = (args: MenuTriggerForDirective) => ({
props: args,
template: `
<div class="tw-h-40">
<button bitButton buttonType="secondary" [bitMenuTriggerFor]="myMenu">Open menu</button>
</div>
<bit-menu #myMenu>
<a href="#" bitMenuItem>Anchor link</a>
<a href="#" bitMenuItem>Another link</a>
<button type="button" bitMenuItem>Button</button>
<bit-menu-divider></bit-menu-divider>
<button type="button" bitMenuItem>Button after divider</button>
</bit-menu>`,
});
export const OpenMenu = Template.bind({});
export const ClosedMenu = TemplateWithButton.bind({});

View File

@@ -0,0 +1,52 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import "zone.js"; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@@ -0,0 +1,128 @@
import { Meta } from "@storybook/addon-docs";
<Meta title="Common/Introduction" />
<style>{`
.subheading {
--mediumdark: '#999999';
font-weight: 900;
font-size: 13px;
color: #999;
letter-spacing: 6px;
line-height: 24px;
text-transform: uppercase;
margin-bottom: 12px;
margin-top: 40px;
}
.link-list {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
row-gap: 10px;
}
@media (min-width: 620px) {
.link-list {
row-gap: 20px;
column-gap: 20px;
grid-template-columns: 1fr 1fr;
}
}
@media all and (-ms-high-contrast:none) {
.link-list {
display: -ms-grid;
-ms-grid-columns: 1fr 1fr;
-ms-grid-rows: 1fr 1fr;
}
}
.link-item {
display: block;
padding: 20px 30px 20px 15px;
border: 1px solid #00000010;
border-radius: 5px;
transition: background 150ms ease-out, border 150ms ease-out, transform 150ms ease-out;
color: #333333;
display: flex;
align-items: flex-start;
}
.link-item:hover {
border-color: #1EA7FD50;
transform: translate3d(0, -3px, 0);
box-shadow: rgba(0, 0, 0, 0.08) 0 3px 10px 0;
}
.link-item:active {
border-color: #1EA7FD;
transform: translate3d(0, 0, 0);
}
.link-item strong {
font-weight: 700;
display: block;
margin-bottom: 2px;
}
.link-item img {
height: 40px;
width: 40px;
margin-right: 15px;
flex: none;
}
.link-item span {
font-size: 14px;
line-height: 20px;
}
`}</style>
# Bitwarden Component Library
The Bitwarden Component Library is a collection of reusable low level components which empowers designers and
developers to work more efficiently. The primary goal is to ensure a consistent design and behavior across the
different clients and platforms. Currently the primary focus is the web based clients, namely _web_, _browser_ and
_desktop_.
**Role out status:** we are currently in the process of transitioning the Web Vault to utilize the component library
and the other clients will follow once this work is completed.
<div className="subheading">Configure</div>
<div className="link-list">
<a className="link-item" href="https://tailwindcss.com/" target="_blank">
<span>
<strong>Tailwind</strong>
The component library CSS is powered by the Tailwind CSS framework
</span>
</a>
<a
className="link-item"
href="https://storybook.js.org/docs/react/get-started/setup#configure-storybook-for-your-stack"
target="_blank"
>
<span>
<strong>Data</strong>
Providers and mocking for data libraries
</span>
</a>
</div>
<div className="subheading">Learn</div>
<div className="link-list">
<a className="link-item" href="https://storybook.js.org/docs" target="_blank">
<span>
<strong>Storybook documentation</strong>
Configure, customize, and extend
</span>
</a>
<a className="link-item" href="https://storybook.js.org/tutorials/" target="_blank">
<span>
<strong>In-depth guides</strong>
Best practices from leading teams
</span>
</a>
</div>

View File

@@ -0,0 +1,86 @@
import { Meta } from "@storybook/addon-docs";
<Meta title="Common/Colors" />
export const Row = (name) => (
<tr class="tw-h-16">
<td class="!tw-border-none">{name}</td>
<td class={"tw-bg-" + name + " !tw-border-secondary-300"}></td>
</tr>
);
export const Table = (args) => (
<table class={"tw-table-auto border !tw-text-main " + args.class}>
<thead>
<tr>
<th>General usage</th>
<th class="tw-w-20"></th>
</tr>
</thead>
<tbody>
{Row("background")}
{Row("background-alt")}
{Row("background-alt2")}
</tbody>
<tbody>
{Row("primary-300")}
{Row("primary-500")}
{Row("primary-700")}
</tbody>
<tbody>
{Row("secondary-100")}
{Row("secondary-300")}
{Row("secondary-500")}
{Row("secondary-700")}
</tbody>
<tbody>
{Row("success-500")}
{Row("success-700")}
</tbody>
<tbody>
{Row("danger-500")}
{Row("danger-700")}
</tbody>
<tbody>
{Row("warning-500")}
{Row("warning-700")}
</tbody>
<tbody>
{Row("info-500")}
{Row("info-700")}
</tbody>
<tbody>
{Row("text-main")}
{Row("text-muted")}
{Row("text-contrast")}
{Row("text-alt2")}
</tbody>
</table>
);
<style>{`
table {
border-spacing: 0.5rem;
border-collapse: separate !important;
}
tr {
background: none !important;
border: none !important;
}
td, th {
color: inherit !important;
}
th {
border: none !important;
}
`}</style>
# Colors
<div class="tw-flex tw-space-x-4">
<Table />
<Table class="theme_dark tw-bg-background" />
</div>

View File

@@ -0,0 +1,11 @@
/* You can add global styles to this file, and also import other style files */
@import "./tw-theme.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-size: 14px;
}

View File

@@ -0,0 +1,44 @@
@import "../../angular/src/scss/webfonts.css";
@import "./variables";
@import "../../angular/src/scss/bwicons/styles/style.scss";
@import "../../angular/src/scss/icons.scss";
@import "@angular/cdk/overlay-prebuilt.css";
@import "~bootstrap/scss/_functions";
@import "~bootstrap/scss/_variables";
@import "~bootstrap/scss/_mixins";
@import "~bootstrap/scss/_root";
@import "~bootstrap/scss/_reboot";
@import "~bootstrap/scss/_type";
@import "~bootstrap/scss/_images";
@import "~bootstrap/scss/_code";
@import "~bootstrap/scss/_grid";
@import "~bootstrap/scss/_tables";
@import "~bootstrap/scss/_forms";
@import "~bootstrap/scss/_buttons";
@import "~bootstrap/scss/_transitions";
@import "~bootstrap/scss/_dropdown";
@import "~bootstrap/scss/_button-group";
@import "~bootstrap/scss/_input-group";
@import "~bootstrap/scss/_custom-forms";
@import "~bootstrap/scss/_nav";
@import "~bootstrap/scss/_navbar";
@import "~bootstrap/scss/_card";
@import "~bootstrap/scss/_breadcrumb";
@import "~bootstrap/scss/_pagination";
@import "~bootstrap/scss/_badge";
@import "~bootstrap/scss/_jumbotron";
@import "~bootstrap/scss/_alert";
@import "~bootstrap/scss/_progress";
@import "~bootstrap/scss/_media";
@import "~bootstrap/scss/_list-group";
@import "~bootstrap/scss/_close";
//@import "~bootstrap/scss/_toasts";
@import "~bootstrap/scss/_modal";
@import "~bootstrap/scss/_tooltip";
@import "~bootstrap/scss/_popover";
@import "~bootstrap/scss/_carousel";
@import "~bootstrap/scss/_spinners";
@import "~bootstrap/scss/_utilities";
@import "~bootstrap/scss/_print";

View File

@@ -0,0 +1,28 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
// eslint-disable-next-line
import "zone.js/testing";
import { getTestBed } from "@angular/core/testing";
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from "@angular/platform-browser-dynamic/testing";
declare const require: {
context(
path: string,
deep?: boolean,
filter?: RegExp
): {
<T>(id: string): T;
keys(): string[];
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
// Then we find all the tests.
const context = require.context("./", true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@@ -0,0 +1,71 @@
:root {
--color-background: #ffffff;
--color-background-alt: #fbfbfb;
--color-background-alt2: #175ddc;
--color-primary-300: #6795e8;
--color-primary-500: #175ddc;
--color-primary-700: #1252a3;
--color-secondary-100: #f0f0f0;
--color-secondary-300: #ced4dc;
--color-secondary-500: #89929f;
--color-secondary-700: #212529;
--color-success-500: #017e45;
--color-success-700: #00552e;
--color-danger-500: #c83522;
--color-danger-700: #98291b;
--color-warning-500: #8b6609;
--color-warning-700: #694d05;
--color-info-500: #555555;
--color-info-700: #3b3a3a;
--color-text-main: #212529;
--color-text-muted: #6d757e;
--color-text-contrast: #ffffff;
--color-text-alt2: #ffffff;
--tw-ring-offset-color: #fff;
}
.theme_light {
/* should be left empty as white is the default */
}
.theme_dark {
--color-background: #1f242e;
--color-background-alt: #161c26;
--color-background-alt2: #2f343d;
--color-primary-300: #175ddc;
--color-primary-500: #6a99f0;
--color-primary-700: #b4ccf9;
--color-secondary-100: #2f343d;
--color-secondary-300: #6e7689;
--color-secondary-500: #bac0ce;
--color-secondary-700: #ffffff;
--color-success-500: #52e07c;
--color-success-700: #a8efbe;
--color-danger-500: #ff8d85;
--color-danger-700: #ffbfbb;
--color-warning-500: #ffeb66;
--color-warning-700: #fff5b3;
--color-info-500: #a4b0c6;
--color-info-700: #d1d7e2;
--color-text-main: #ffffff;
--color-text-muted: #bac0ce;
--color-text-contrast: #191e26;
--color-text-alt2: #ffffff;
--tw-ring-offset-color: #1f242e;
}

View File

@@ -0,0 +1,19 @@
import { I18nService } from "jslib-common/abstractions/i18n.service";
export class I18nMockService implements I18nService {
locale: string;
supportedTranslationLocales: string[];
translationLocale: string;
collator: Intl.Collator;
localeNames: Map<string, string>;
constructor(private lookupTable: Record<string, string>) {}
t(id: string, p1?: string, p2?: string, p3?: string) {
return this.lookupTable[id];
}
translate(id: string, p1?: string, p2?: string, p3?: string) {
return this.t(id, p1, p2, p3);
}
}

View File

@@ -0,0 +1,357 @@
$dark-icon-themes: "theme_dark";
$primary: #175ddc;
$primary-accent: #1252a3;
$secondary: #ced4da;
$secondary-alt: #1a3b66;
$success: #00a65a;
$info: #555555;
$warning: #bf7e16;
$danger: #dd4b39;
$white: #ffffff;
// Bootstrap Variable Overrides
$theme-colors: (
"primary-accent": $primary-accent,
"secondary-alt": $secondary-alt,
);
$body-bg: $white;
$body-color: #333333;
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
$h1-font-size: 1.7rem;
$h2-font-size: 1.3rem;
$h3-font-size: 1rem;
$h4-font-size: 1rem;
$h5-font-size: 1rem;
$h6-font-size: 1rem;
$small-font-size: 90%;
$font-size-lg: 1.15rem;
$code-font-size: 100%;
$navbar-padding-y: 0.75rem;
$grid-gutter-width: 20px;
$card-spacer-y: 0.6rem;
$list-group-item-padding-y: 0.6rem;
$list-group-active-color: $body-color;
$list-group-active-bg: $white;
$list-group-active-border-color: rgba(#000000, 0.125);
$dropdown-link-color: $body-color;
$dropdown-link-hover-bg: rgba(#000000, 0.06);
$dropdown-link-active-color: $dropdown-link-color;
$dropdown-link-active-bg: rgba(#000000, 0.1);
$dropdown-item-padding-x: 1rem;
$navbar-brand-font-size: 35px;
$navbar-brand-height: 35px;
$navbar-brand-padding-y: 0;
$navbar-dark-color: rgba($white, 0.7);
$navbar-dark-hover-color: rgba($white, 0.9);
$navbar-nav-link-padding-x: 0.8rem;
$input-bg: #fbfbfb;
$input-focus-bg: $white;
$input-disabled-bg: #e0e0e0;
$input-placeholder-color: #b4b4b4;
$table-accent-bg: rgba(#000000, 0.02);
$table-hover-bg: rgba(#000000, 0.03);
$modal-backdrop-opacity: 0.3;
$btn-font-weight: 600;
$lead-font-weight: normal;
$grid-breakpoints: (
xs: 0,
sm: 1px,
md: 2px,
lg: 3px,
xl: 4px,
);
$border-color: $secondary;
// MFA Types for logo styling with no dark theme alternative
$mfaTypes: 0, 2, 3, 4, 6;
// Theme Variables
// Light
$lightDangerHover: #c43421;
$lightInputColor: #465057;
$lightInputPlaceholderColor: #b6b8b8;
// Dark
$darkPrimary: #6a99f0;
$darkPrimary-alt: #b4ccf9;
$darkDanger: #ff8d85;
$darkDangerHover: #ffbfbb;
$darkSuccess: #52e07c;
$darkWarning: #ffeb66;
$darkInfo: #a4b0c6;
$darkLinks: #6a99f0;
$darkGrey1: #bac0ce;
$darkGrey2: #8d94a5;
$darkBlue1: #4c525f;
$darkBlue2: #3c424e;
$darkDarkBlue1: #2f343d;
$darkDarkBlue2: #1f242e;
$darkInputColor: $white;
$darkInputPlaceholderColor: $darkGrey1;
$themes: (
light: (
primary: $primary,
primaryAlt: $primary-accent,
danger: $danger,
info: #343a40,
success: $success,
warning: $warning,
backgroundColor: $white,
badgeDangerBackground: $danger,
badgeDangerText: $white,
badgeInfoBackground: #555555,
badgeInfoText: $white,
badgePrimaryBackground: $primary,
badgePrimaryBackgroundHover: #134eb9,
badgePrimaryText: $white,
badgeSecondaryBackground: #ced4da,
badgeSecondaryText: #212529,
bgLightColor: #f8f9fa,
bgPrimaryColor: $primary,
borderColor: $border-color,
borderPrimaryColor: $primary,
browserInputIconsFilter: invert(0),
btnDanger: $danger,
btnDangerHover: $lightDangerHover,
btnDangerText: $white,
btnLinkText: $primary,
btnLinkTextHover: #104097,
btnOutlineDangerBackground: $input-bg,
btnOutlineDangerBackgroundHover: $danger,
btnOutlineDangerBorder: #ced4da,
btnOutlineDangerBorderHover: $danger,
btnOutlineDangerText: $danger,
btnOutlineDangerTextHover: $white,
btnOutlinePrimaryBackground: $input-bg,
btnOutlinePrimaryBackgroundHover: $primary,
btnOutlinePrimaryBorder: #ced4da,
btnOutlinePrimaryBorderHover: $primary,
btnOutlinePrimaryText: $primary,
btnOutlinePrimaryTextHover: $white,
btnOutlineSecondaryBackground: $input-bg,
btnOutlineSecondaryBackgroundHover: #ced4da,
btnOutlineSecondaryBorder: #ced4da,
btnOutlineSecondaryBorderHover: #ced4da,
btnOutlineSecondaryText: #6c757d,
btnOutlineSecondaryTextHover: #333333,
btnPrimary: $primary,
btnPrimaryBorderHover: #1249ae,
btnPrimaryHover: #134eb9,
btnPrimaryText: $white,
btnSecondary: $secondary,
btnSecondaryBorder: $secondary,
btnSecondaryBorderHover: #b1bbc4,
btnSecondaryHover: #b8c1ca,
btnSecondaryText: #212529,
btnSecondaryTextHover: #212529,
calloutBackground: #fafafa,
calloutColor: #212529,
cdkDraggingBackground: $white,
codeColor: #e83e8c,
dropdownBackground: $white,
dropdownHover: rgba(0, 0, 0, 0.06),
dropdownTextColor: $body-color,
dropdownTextMuted: #6c757d,
focus: rgb(23 93 220 / 25%),
footerBackgroundColor: #fbfbfb,
foregroundColor: $white,
headerColor: rgba(0, 0, 0, 0.03),
iconColor: #777777,
iconHover: $body-color,
imgFilter: invert(0) grayscale(0),
inputBackgroundColor: $input-bg,
inputBorderColor: $border-color,
inputDisabledBackground: #e0e0e0,
inputDisabledColor: #6c757d,
inputPlaceholderColor: $lightInputPlaceholderColor,
inputTextColor: $lightInputColor,
layoutFrontendColor: #ecf0f5,
learnMoreHover: #104097,
linkColor: $primary,
linkColorHover: #104097,
linkWeight: 400,
listItemActive: $body-color,
listItemBorder: rgba(0, 0, 0, 0.125),
loadingSvg: url("../images/loading.svg"),
logoSuffix: "dark",
mfaLogoSuffix: ".png",
navActiveBackground: $white,
navActiveWeight: 600,
navBackground: $primary,
navBackgroundAlt: $secondary-alt,
navOrgBackgroundColor: #fbfbfb,
navWeight: 600,
pwLetter: $body-color,
pwNumber: #007fde,
pwSpecial: #c40800,
pwStrengthBackground: #e9ecef,
separator: $secondary,
separatorHr: rgb(0, 0, 0, 0.1),
tableColorHover: #333333,
tableLinkColor: $primary,
tableLinkColorHover: #104097,
tableRowHover: rgba(0, 0, 0, 0.03),
tableSeparator: #dee2e6,
textColor: $body-color,
textDangerColor: $white,
textInfoColor: $white,
textHeadingColor: #333333,
textMuted: #6c757d,
textSuccessColor: $white,
textWarningColor: $white,
),
dark: (
primary: $darkPrimary,
primaryAlt: $darkPrimary-alt,
danger: $darkDanger,
info: $darkInfo,
success: $darkSuccess,
warning: $darkWarning,
backgroundColor: $darkDarkBlue2,
badgeDangerBackground: $darkDanger,
badgeDangerText: $darkDarkBlue2,
badgeInfoBackground: $darkInfo,
badgeInfoText: $darkDarkBlue2,
badgePrimaryBackground: $darkLinks,
badgePrimaryBackgroundHover: $darkPrimary-alt,
badgePrimaryText: $darkDarkBlue2,
badgeSecondaryBackground: $darkGrey2,
badgeSecondaryText: $darkDarkBlue2,
bgLightColor: $darkDarkBlue2,
bgPrimaryColor: $darkPrimary,
borderColor: $darkBlue1,
borderPrimaryColor: $darkPrimary,
browserInputIconsFilter: invert(1),
btnDanger: $darkDanger,
btnDangerHover: $darkDangerHover,
btnDangerText: $darkDarkBlue2,
btnLinkText: $white,
btnLinkTextHover: $darkGrey1,
btnOutlineDangerBackground: $darkDanger,
btnOutlineDangerBackgroundHover: $darkDangerHover,
btnOutlineDangerBorder: $darkDanger,
btnOutlineDangerBorderHover: $darkDangerHover,
btnOutlineDangerText: $darkDarkBlue2,
btnOutlineDangerTextHover: $darkDarkBlue2,
btnOutlinePrimaryBackground: $darkPrimary,
btnOutlinePrimaryBackgroundHover: $darkPrimary-alt,
btnOutlinePrimaryBorder: $darkPrimary,
btnOutlinePrimaryBorderHover: $darkPrimary-alt,
btnOutlinePrimaryText: $darkDarkBlue2,
btnOutlinePrimaryTextHover: $darkDarkBlue2,
btnOutlineSecondaryBackground: transparent,
btnOutlineSecondaryBackgroundHover: transparent,
btnOutlineSecondaryBorder: $darkGrey1,
btnOutlineSecondaryBorderHover: $darkGrey2,
btnOutlineSecondaryText: $white,
btnOutlineSecondaryTextHover: $darkGrey2,
btnPrimary: $darkLinks,
btnPrimaryBorderHover: $darkPrimary-alt,
btnPrimaryHover: $darkPrimary-alt,
btnPrimaryText: $darkDarkBlue2,
btnSecondary: transparent,
btnSecondaryBorder: $darkGrey1,
btnSecondaryBorderHover: $darkGrey2,
btnSecondaryHover: transparent,
btnSecondaryText: $white,
btnSecondaryTextHover: $darkGrey2,
calloutBackground: $darkBlue2,
calloutColor: $white,
cdkDraggingBackground: $darkDarkBlue1,
codeColor: #e83e8c,
dropdownBackground: $darkDarkBlue1,
dropdownHover: rgba(255, 255, 255, 0.03),
dropdownTextColor: $white,
dropdownTextMuted: #bec6cf,
focus: rgb(106 153 240 / 25%),
footerBackgroundColor: $darkBlue1,
foregroundColor: $darkDarkBlue1,
headerColor: $darkBlue1,
iconColor: #777777,
iconHover: $darkGrey2,
imgFilter: invert(1) grayscale(1),
inputBackgroundColor: transparent,
inputBorderColor: $darkGrey1,
inputDisabledBackground: $darkBlue2,
inputDisabledColor: $darkGrey1,
inputPlaceholderColor: $darkInputPlaceholderColor,
inputTextColor: $darkInputColor,
layoutFrontendColor: $darkDarkBlue2,
learnMoreHover: $darkPrimary-alt,
linkColor: $darkLinks,
linkColorHover: $darkLinks,
linkWeight: 600,
listItemActive: $darkPrimary,
listItemBorder: $darkBlue1,
loadingSvg: url("../images/loading-white.svg"),
logoSuffix: "white",
mfaLogoSuffix: "-w.png",
navActiveBackground: $darkDarkBlue2,
navActiveWeight: 600,
navBackground: $darkDarkBlue1,
navBackgroundAlt: $darkDarkBlue1,
navOrgBackgroundColor: #161c26,
navWeight: 400,
pwLetter: $white,
pwNumber: #52bdfb,
pwSpecial: #ff7c70,
pwStrengthBackground: $darkBlue2,
separator: $darkBlue1,
separatorHr: $darkBlue1,
tableColorHover: $darkGrey1,
tableLinkColor: $white,
tableLinkColorHover: $white,
tableRowHover: rgba(255, 255, 255, 0.03),
tableSeparator: $darkBlue1,
textColor: $darkGrey1,
textDangerColor: $darkDarkBlue2,
textHeadingColor: $white,
textInfoColor: $darkDarkBlue2,
textMuted: $darkGrey1,
textSuccessColor: $darkDarkBlue2,
textWarningColor: $darkDarkBlue2,
),
);
@mixin themify($themes: $themes) {
@each $theme, $map in $themes {
html.theme_#{$theme} & {
$theme-map: () !global;
@each $key, $submap in $map {
$value: map-get(map-get($themes, $theme), "#{$key}");
$theme-map: map-merge(
$theme-map,
(
$key: $value,
)
) !global;
}
@content;
$theme-map: null !global;
}
}
}
@function themed($key) {
@return map-get($theme-map, $key);
} ;