1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

PoC class variance authority

This commit is contained in:
Hinton
2025-03-25 18:53:33 +09:00
parent 8ed8c9af6a
commit 4928ec54df
4 changed files with 186 additions and 108 deletions

View File

@@ -2,32 +2,100 @@
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, ElementRef, HostBinding, Input } from "@angular/core";
import { VariantProps, cva } from "class-variance-authority";
import { FocusableElement } from "../shared/focusable-element";
export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info";
export type BadgeVariant = VariantProps<typeof badge>["variant"];
const styles: Record<BadgeVariant, string[]> = {
primary: ["tw-bg-primary-100", "tw-border-primary-700", "!tw-text-primary-700"],
secondary: ["tw-bg-secondary-100", "tw-border-secondary-700", "!tw-text-secondary-700"],
success: ["tw-bg-success-100", "tw-border-success-700", "!tw-text-success-700"],
danger: ["tw-bg-danger-100", "tw-border-danger-700", "!tw-text-danger-700"],
warning: ["tw-bg-warning-100", "tw-border-warning-700", "!tw-text-warning-700"],
info: ["tw-bg-info-100", "tw-border-info-700", "!tw-text-info-700"],
};
const hoverStyles: Record<BadgeVariant, string[]> = {
primary: ["hover:tw-bg-primary-600", "hover:tw-border-primary-600", "hover:!tw-text-contrast"],
secondary: [
"hover:tw-bg-secondary-600",
"hover:tw-border-secondary-600",
"hover:!tw-text-contrast",
const badge = cva(
[
"tw-inline-block",
"tw-py-1",
"tw-px-2",
"tw-font-medium",
"tw-text-center",
"tw-align-text-top",
"tw-rounded-full",
"tw-border-[0.5px]",
"tw-border-solid",
"tw-box-border",
"tw-whitespace-nowrap",
"tw-text-xs",
"hover:tw-no-underline",
"focus-visible:tw-outline-none",
"focus-visible:tw-ring-2",
"focus-visible:tw-ring-offset-2",
"focus-visible:tw-ring-primary-600",
"disabled:tw-bg-secondary-300",
"disabled:hover:tw-bg-secondary-300",
"disabled:tw-border-secondary-300",
"disabled:hover:tw-border-secondary-300",
"disabled:!tw-text-muted",
"disabled:hover:!tw-text-muted",
"disabled:tw-cursor-not-allowed",
],
success: ["hover:tw-bg-success-600", "hover:tw-border-success-600", "hover:!tw-text-contrast"],
danger: ["hover:tw-bg-danger-600", "hover:tw-border-danger-600", "hover:!tw-text-contrast"],
warning: ["hover:tw-bg-warning-600", "hover:tw-border-warning-600", "hover:!tw-text-black"],
info: ["hover:tw-bg-info-600", "hover:tw-border-info-600", "hover:!tw-text-black"],
};
{
variants: {
variant: {
primary: ["tw-bg-primary-100", "tw-border-primary-700", "!tw-text-primary-700"],
secondary: ["tw-bg-secondary-100", "tw-border-secondary-700", "!tw-text-secondary-700"],
success: ["tw-bg-success-100", "tw-border-success-700", "!tw-text-success-700"],
danger: ["tw-bg-danger-100", "tw-border-danger-700", "!tw-text-danger-700"],
warning: ["tw-bg-warning-100", "tw-border-warning-700", "!tw-text-warning-700"],
info: ["tw-bg-info-100", "tw-border-info-700", "!tw-text-info-700"],
},
hover: {
true: null,
false: null,
},
},
compoundVariants: [
{
variant: "primary",
hover: true,
class: [
"hover:tw-bg-primary-600",
"hover:tw-border-primary-600",
"hover:!tw-text-contrast",
],
},
{
variant: "secondary",
hover: true,
class: [
"hover:tw-bg-secondary-600",
"hover:tw-border-secondary-600",
"hover:!tw-text-contrast",
],
},
{
variant: "success",
hover: true,
class: [
"hover:tw-bg-success-600",
"hover:tw-border-success-600",
"hover:!tw-text-contrast",
],
},
{
variant: "danger",
hover: true,
class: ["hover:tw-bg-danger-600", "hover:tw-border-danger-600", "hover:!tw-text-contrast"],
},
{
variant: "warning",
hover: true,
class: ["hover:tw-bg-warning-600", "hover:tw-border-warning-600", "hover:!tw-text-black"],
},
{
variant: "info",
hover: true,
class: ["hover:tw-bg-info-600", "hover:tw-border-info-600", "hover:!tw-text-black"],
},
],
},
);
@Component({
selector: "span[bitBadge], a[bitBadge], button[bitBadge]",
@@ -38,36 +106,13 @@ const hoverStyles: Record<BadgeVariant, string[]> = {
})
export class BadgeComponent implements FocusableElement {
@HostBinding("class") get classList() {
return [
"tw-inline-block",
"tw-py-1",
"tw-px-2",
"tw-font-medium",
"tw-text-center",
"tw-align-text-top",
"tw-rounded-full",
"tw-border-[0.5px]",
"tw-border-solid",
"tw-box-border",
"tw-whitespace-nowrap",
"tw-text-xs",
"hover:tw-no-underline",
"focus-visible:tw-outline-none",
"focus-visible:tw-ring-2",
"focus-visible:tw-ring-offset-2",
"focus-visible:tw-ring-primary-600",
"disabled:tw-bg-secondary-300",
"disabled:hover:tw-bg-secondary-300",
"disabled:tw-border-secondary-300",
"disabled:hover:tw-border-secondary-300",
"disabled:!tw-text-muted",
"disabled:hover:!tw-text-muted",
"disabled:tw-cursor-not-allowed",
]
.concat(styles[this.variant])
.concat(this.hasHoverEffects ? [...hoverStyles[this.variant], "tw-min-w-10"] : [])
.concat(this.truncate ? this.maxWidthClass : []);
return badge({
variant: this.variant,
hover: this.hasHoverEffects,
class: this.truncate ? this.maxWidthClass : undefined,
});
}
@HostBinding("attr.title") get titleAttr() {
if (this.title !== undefined) {
return this.title;

View File

@@ -4,6 +4,7 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { NgClass } from "@angular/common";
import { Input, HostBinding, Component, model, computed } from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { cva } from "class-variance-authority";
import { debounce, interval } from "rxjs";
import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
@@ -14,36 +15,71 @@ const focusRing = [
"focus-visible:tw-ring-primary-600",
"focus-visible:tw-z-10",
];
const buttonStyles: Record<ButtonType, string[]> = {
primary: [
"tw-border-primary-600",
"tw-bg-primary-600",
"!tw-text-contrast",
"hover:tw-bg-primary-700",
"hover:tw-border-primary-700",
...focusRing,
const button = cva(
[
"tw-font-semibold",
"tw-py-1.5",
"tw-px-3",
"tw-rounded-full",
"tw-transition",
"tw-border-2",
"tw-border-solid",
"tw-text-center",
"tw-no-underline",
"hover:tw-no-underline",
"focus:tw-outline-none",
],
secondary: [
"tw-bg-transparent",
"tw-border-primary-600",
"!tw-text-primary-600",
"hover:tw-bg-primary-600",
"hover:tw-border-primary-600",
"hover:!tw-text-contrast",
...focusRing,
],
danger: [
"tw-bg-transparent",
"tw-border-danger-600",
"!tw-text-danger",
"hover:tw-bg-danger-600",
"hover:tw-border-danger-600",
"hover:!tw-text-contrast",
...focusRing,
],
unstyled: [],
};
{
variants: {
variant: {
primary: [
"tw-border-primary-600",
"tw-bg-primary-600",
"!tw-text-contrast",
"hover:tw-bg-primary-700",
"hover:tw-border-primary-700",
...focusRing,
],
secondary: [
"tw-bg-transparent",
"tw-border-primary-600",
"!tw-text-primary-600",
"hover:tw-bg-primary-600",
"hover:tw-border-primary-600",
"hover:!tw-text-contrast",
...focusRing,
],
danger: [
"tw-bg-transparent",
"tw-border-danger-600",
"!tw-text-danger",
"hover:tw-bg-danger-600",
"hover:tw-border-danger-600",
"hover:!tw-text-contrast",
...focusRing,
],
unstyled: [],
},
disabled: {
true: [
"disabled:tw-bg-secondary-300",
"disabled:hover:tw-bg-secondary-300",
"disabled:tw-border-secondary-300",
"disabled:hover:tw-border-secondary-300",
"disabled:!tw-text-muted",
"disabled:hover:!tw-text-muted",
"disabled:tw-cursor-not-allowed",
"disabled:hover:tw-no-underline",
],
false: null,
},
block: {
true: ["tw-w-full", "tw-block"],
false: ["tw-inline-block"],
},
},
},
);
@Component({
selector: "button[bitButton], a[bitButton]",
@@ -57,35 +93,11 @@ const buttonStyles: Record<ButtonType, string[]> = {
})
export class ButtonComponent implements ButtonLikeAbstraction {
@HostBinding("class") get classList() {
return [
"tw-font-semibold",
"tw-py-1.5",
"tw-px-3",
"tw-rounded-full",
"tw-transition",
"tw-border-2",
"tw-border-solid",
"tw-text-center",
"tw-no-underline",
"hover:tw-no-underline",
"focus:tw-outline-none",
]
.concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
.concat(buttonStyles[this.buttonType ?? "secondary"])
.concat(
this.showDisabledStyles() || this.disabled()
? [
"disabled:tw-bg-secondary-300",
"disabled:hover:tw-bg-secondary-300",
"disabled:tw-border-secondary-300",
"disabled:hover:tw-border-secondary-300",
"disabled:!tw-text-muted",
"disabled:hover:!tw-text-muted",
"disabled:tw-cursor-not-allowed",
"disabled:hover:tw-no-underline",
]
: [],
);
return button({
variant: this.buttonType ?? "secondary",
block: this.block,
disabled: this.showDisabledStyles() || this.disabled(),
});
}
protected disabledAttr = computed(() => {

20
package-lock.json generated
View File

@@ -40,6 +40,7 @@
"buffer": "6.0.3",
"bufferutil": "4.0.9",
"chalk": "4.1.2",
"class-variance-authority": "0.7.1",
"commander": "11.1.0",
"core-js": "3.40.0",
"form-data": "4.0.1",
@@ -15771,6 +15772,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"dependencies": {
"clsx": "^2.1.1"
},
"funding": {
"url": "https://polar.sh/cva"
}
},
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@@ -15939,6 +15951,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",

View File

@@ -170,6 +170,7 @@
"buffer": "6.0.3",
"bufferutil": "4.0.9",
"chalk": "4.1.2",
"class-variance-authority": "0.7.1",
"commander": "11.1.0",
"core-js": "3.40.0",
"form-data": "4.0.1",