mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
Move to libs
This commit is contained in:
9
libs/components/src/app/app.component.ts
Normal file
9
libs/components/src/app/app.component.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
template: "",
|
||||
})
|
||||
export class AppComponent {
|
||||
title = "components";
|
||||
}
|
||||
13
libs/components/src/app/app.module.ts
Normal file
13
libs/components/src/app/app.module.ts
Normal 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 {}
|
||||
58
libs/components/src/badge/badge.directive.ts
Normal file
58
libs/components/src/badge/badge.directive.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
11
libs/components/src/badge/badge.module.ts
Normal file
11
libs/components/src/badge/badge.module.ts
Normal 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 {}
|
||||
56
libs/components/src/badge/badge.stories.ts
Normal file
56
libs/components/src/badge/badge.stories.ts
Normal 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",
|
||||
};
|
||||
2
libs/components/src/badge/index.ts
Normal file
2
libs/components/src/badge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./badge.directive";
|
||||
export * from "./badge.module";
|
||||
14
libs/components/src/banner/banner.component.html
Normal file
14
libs/components/src/banner/banner.component.html
Normal 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>
|
||||
35
libs/components/src/banner/banner.component.spec.ts
Normal file
35
libs/components/src/banner/banner.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
39
libs/components/src/banner/banner.component.ts
Normal file
39
libs/components/src/banner/banner.component.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
11
libs/components/src/banner/banner.module.ts
Normal file
11
libs/components/src/banner/banner.module.ts
Normal 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 {}
|
||||
38
libs/components/src/banner/banner.stories.ts
Normal file
38
libs/components/src/banner/banner.stories.ts
Normal 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",
|
||||
};
|
||||
2
libs/components/src/banner/index.ts
Normal file
2
libs/components/src/banner/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./banner.component";
|
||||
export * from "./banner.module";
|
||||
77
libs/components/src/button/button.directive.spec.ts
Normal file
77
libs/components/src/button/button.directive.spec.ts
Normal 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;
|
||||
}
|
||||
72
libs/components/src/button/button.directive.ts
Normal file
72
libs/components/src/button/button.directive.ts
Normal 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;
|
||||
}
|
||||
11
libs/components/src/button/button.module.ts
Normal file
11
libs/components/src/button/button.module.ts
Normal 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 {}
|
||||
54
libs/components/src/button/button.stories.ts
Normal file
54
libs/components/src/button/button.stories.ts
Normal 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",
|
||||
};
|
||||
2
libs/components/src/button/index.ts
Normal file
2
libs/components/src/button/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./button.directive";
|
||||
export * from "./button.module";
|
||||
14
libs/components/src/callout/callout.component.html
Normal file
14
libs/components/src/callout/callout.component.html
Normal 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>
|
||||
64
libs/components/src/callout/callout.component.spec.ts
Normal file
64
libs/components/src/callout/callout.component.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
63
libs/components/src/callout/callout.component.ts
Normal file
63
libs/components/src/callout/callout.component.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
11
libs/components/src/callout/callout.module.ts
Normal file
11
libs/components/src/callout/callout.module.ts
Normal 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 {}
|
||||
65
libs/components/src/callout/callout.stories.ts
Normal file
65
libs/components/src/callout/callout.stories.ts
Normal 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",
|
||||
};
|
||||
2
libs/components/src/callout/index.ts
Normal file
2
libs/components/src/callout/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./callout.module";
|
||||
export * from "./callout.component";
|
||||
BIN
libs/components/src/favicon.ico
Normal file
BIN
libs/components/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
39
libs/components/src/form-field/error-summary.component.ts
Normal file
39
libs/components/src/form-field/error-summary.component.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
78
libs/components/src/form-field/error-summary.stories.ts
Normal file
78
libs/components/src/form-field/error-summary.stories.ts
Normal 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 = {};
|
||||
38
libs/components/src/form-field/error.component.ts
Normal file
38
libs/components/src/form-field/error.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
libs/components/src/form-field/form-field.component.html
Normal file
17
libs/components/src/form-field/form-field.component.html
Normal 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>
|
||||
53
libs/components/src/form-field/form-field.component.ts
Normal file
53
libs/components/src/form-field/form-field.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
libs/components/src/form-field/form-field.module.ts
Normal file
54
libs/components/src/form-field/form-field.module.ts
Normal 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 {}
|
||||
212
libs/components/src/form-field/form-field.stories.ts
Normal file
212
libs/components/src/form-field/form-field.stories.ts
Normal 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 = {};
|
||||
14
libs/components/src/form-field/hint.component.ts
Normal file
14
libs/components/src/form-field/hint.component.ts
Normal 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++}`;
|
||||
}
|
||||
2
libs/components/src/form-field/index.ts
Normal file
2
libs/components/src/form-field/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./form-field.module";
|
||||
export * from "./form-field.component";
|
||||
6
libs/components/src/form-field/label.directive.ts
Normal file
6
libs/components/src/form-field/label.directive.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Directive } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "bit-label",
|
||||
})
|
||||
export class BitLabel {}
|
||||
28
libs/components/src/form-field/prefix.directive.ts
Normal file
28
libs/components/src/form-field/prefix.directive.ts
Normal 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;
|
||||
}
|
||||
18
libs/components/src/form-field/suffix.directive.ts
Normal file
18
libs/components/src/form-field/suffix.directive.ts
Normal 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;
|
||||
}
|
||||
13
libs/components/src/index.html
Normal file
13
libs/components/src/index.html
Normal 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>
|
||||
6
libs/components/src/index.ts
Normal file
6
libs/components/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./badge";
|
||||
export * from "./banner";
|
||||
export * from "./button";
|
||||
export * from "./callout";
|
||||
export * from "./form-field";
|
||||
export * from "./menu";
|
||||
65
libs/components/src/input/input.directive.ts
Normal file
65
libs/components/src/input/input.directive.ts
Normal 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) {}
|
||||
}
|
||||
11
libs/components/src/input/input.module.ts
Normal file
11
libs/components/src/input/input.module.ts
Normal 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 {}
|
||||
7
libs/components/src/main.ts
Normal file
7
libs/components/src/main.ts
Normal 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
|
||||
5
libs/components/src/menu/index.ts
Normal file
5
libs/components/src/menu/index.ts
Normal 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";
|
||||
4
libs/components/src/menu/menu-divider.component.html
Normal file
4
libs/components/src/menu/menu-divider.component.html
Normal 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>
|
||||
7
libs/components/src/menu/menu-divider.component.ts
Normal file
7
libs/components/src/menu/menu-divider.component.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "bit-menu-divider",
|
||||
templateUrl: "./menu-divider.component.html",
|
||||
})
|
||||
export class MenuDividerComponent {}
|
||||
36
libs/components/src/menu/menu-item.directive.ts
Normal file
36
libs/components/src/menu/menu-item.directive.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
119
libs/components/src/menu/menu-trigger-for.directive.ts
Normal file
119
libs/components/src/menu/menu-trigger-for.directive.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
9
libs/components/src/menu/menu.component.html
Normal file
9
libs/components/src/menu/menu.component.html
Normal 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>
|
||||
77
libs/components/src/menu/menu.component.spec.ts
Normal file
77
libs/components/src/menu/menu.component.spec.ts
Normal 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 {}
|
||||
30
libs/components/src/menu/menu.component.ts
Normal file
30
libs/components/src/menu/menu.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
15
libs/components/src/menu/menu.module.ts
Normal file
15
libs/components/src/menu/menu.module.ts
Normal 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 {}
|
||||
69
libs/components/src/menu/menu.stories.ts
Normal file
69
libs/components/src/menu/menu.stories.ts
Normal 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({});
|
||||
52
libs/components/src/polyfills.ts
Normal file
52
libs/components/src/polyfills.ts
Normal 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
|
||||
*/
|
||||
128
libs/components/src/stories/Introduction.stories.mdx
Normal file
128
libs/components/src/stories/Introduction.stories.mdx
Normal 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>
|
||||
86
libs/components/src/stories/colors.stories.mdx
Normal file
86
libs/components/src/stories/colors.stories.mdx
Normal 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>
|
||||
11
libs/components/src/styles.css
Normal file
11
libs/components/src/styles.css
Normal 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;
|
||||
}
|
||||
44
libs/components/src/styles.scss
Normal file
44
libs/components/src/styles.scss
Normal 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";
|
||||
28
libs/components/src/test.ts
Normal file
28
libs/components/src/test.ts
Normal 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);
|
||||
71
libs/components/src/tw-theme.css
Normal file
71
libs/components/src/tw-theme.css
Normal 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;
|
||||
}
|
||||
19
libs/components/src/utils/i18n-mock.service.ts
Normal file
19
libs/components/src/utils/i18n-mock.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
357
libs/components/src/variables.scss
Normal file
357
libs/components/src/variables.scss
Normal 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);
|
||||
} ;
|
||||
Reference in New Issue
Block a user