button-component #29
@ -1,7 +1,7 @@
|
|||||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||||
|
|
||||||
export type ButtonVariant = 'primary' | 'secondary' | 'red' | 'green';
|
export type ButtonVariant = "primary" | "secondary" | "red" | "green";
|
||||||
export type ButtonSize = 'sm' | 'md' | 'lg';
|
export type ButtonSize = "sm" | "md" | "lg";
|
||||||
|
|
||||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
@ -10,32 +10,35 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const variantClasses: Record<ButtonVariant, string> = {
|
const variantClasses: Record<ButtonVariant, string> = {
|
||||||
primary: 'bg-primary text-base-canvas border border-primary hover:bg-secondary hover:text-base-canvas hover:border-primary focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100',
|
primary:
|
||||||
secondary: 'bg-base-canvas text-base-ink-strong border border-base-ink-soft hover:bg-base-canvas hover:text-base-ink-strong hover:border-base-ink-medium focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100',
|
"bg-primary text-base-canvas border border-primary hover:bg-secondary hover:text-base-canvas hover:border hover:border-primary focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border focus-visible:border-primary focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100",
|
||||||
|
stne3960 marked this conversation as resolved
Outdated
|
|||||||
red: 'bg-other-red-100 text-su-white border border-other-red-100 focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100',
|
secondary:
|
||||||
green: 'bg-other-green text-su-white border border-other-green focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100',
|
"bg-base-canvas text-base-ink-strong border-solid [border-width:var(--border-width-sm)] border-base-ink-soft hover:bg-base-canvas hover:text-base-ink-strong hover:border-base-ink-medium focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100",
|
||||||
|
red: "bg-other-red-100 text-su-white focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:border focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100",
|
||||||
|
green:
|
||||||
|
"bg-other-green text-su-white focus-visible:bg-base-canvas focus-visible:text-base-ink-strong focus-visible:border-primary focus-visible:border focus-visible:outline focus-visible:outline-[length:var(--border-width-lg)] focus-visible:outline-sky-100",
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses: Record<ButtonSize, string> = {
|
const sizeClasses: Record<ButtonSize, string> = {
|
||||||
sm: 'h-(--control-height-sm) min-w-(--button-min-width-sm) px-(--button-padding-x-sm) body-normal-md rounded-(--border-radius-sm)',
|
sm: "h-(--control-height-sm) min-w-(--button-min-width-sm) px-(--button-padding-x-sm) body-bold-md rounded-(--border-radius-sm)",
|
||||||
md: 'h-(--control-height-md) min-w-(--button-min-width-md) px-(--button-padding-x-md) body-normal-md rounded-(--border-radius-sm)',
|
md: "h-(--control-height-md) min-w-(--button-min-width-md) px-(--button-padding-x-md) body-bold-md rounded-(--border-radius-sm)",
|
||||||
lg: 'h-(--control-height-lg) min-w-(--button-min-width-lg) px-(--button-padding-x-lg) body-normal-lg rounded-(--border-radius-md)',
|
lg: "h-(--control-height-lg) min-w-(--button-min-width-lg) px-(--button-padding-x-lg) body-bold-lg rounded-(--border-radius-md)",
|
||||||
};
|
};
|
||||||
|
|
||||||
const textPaddingClasses: Record<ButtonSize, string> = {
|
const textPaddingClasses: Record<ButtonSize, string> = {
|
||||||
sm: 'px-(--button-text-padding-x-sm)',
|
sm: "px-(--button-text-padding-x-sm)",
|
||||||
md: 'px-(--button-text-padding-x-md)',
|
md: "px-(--button-text-padding-x-md)",
|
||||||
lg: 'px-(--button-text-padding-x-lg)',
|
lg: "px-(--button-text-padding-x-lg)",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Button({
|
export default function Button({
|
||||||
variant = 'primary',
|
variant = "primary",
|
||||||
size = 'md',
|
size = "md",
|
||||||
className = '',
|
className = "",
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
const baseClasses = 'inline-flex items-center justify-center cursor-pointer';
|
const baseClasses = "inline-flex items-center justify-center cursor-pointer";
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
baseClasses,
|
baseClasses,
|
||||||
@ -44,13 +47,10 @@ export default function Button({
|
|||||||
className,
|
className,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
stne3960 marked this conversation as resolved
Outdated
ansv7779
commented
What does this do? I assume it does some JavaScript type-coercion of strings/ My initial read through of the code, none of the strings in the list should be What does this do? I assume it does some JavaScript type-coercion of strings/`undefined`/`null` to booleans but what about the empty string? Needs a clarification comment.
My initial read through of the code, none of the strings in the list should be `null` or `undefined` and joining an empty string shouldn't matter much.
stne3960
commented
You're right that the current implementation passes only non-empty strings, so The reason it's useful is as soon as someone adds conditional classes or optional props (e.g. Using You're right that the current implementation passes only non-empty strings, so `.filter(Boolean)` doesn't change behavior today.
The reason it's useful is as soon as someone adds conditional classes or optional props (e.g. `isDisabled && "opacity-50 cursor-not-allowed"`), it's easy for the array to end up containing false, undefined, or empty strings. Without filtering, those values can end up in the final className and produce a noisy DOM.
Using `.filter(Boolean)` keeps the class string clean no matter how the component evolves. I'll add a short comment explaining this so future contributors understand the intent.
|
|||||||
.join(' ');
|
.join(" ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button className={classes} {...props}>
|
||||||
className={classes}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className={textPaddingClasses[size]}>{children}</span>
|
<span className={textPaddingClasses[size]}>{children}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,86 +2,86 @@
|
|||||||
|
|
||||||
/* TheSans Font Family */
|
/* TheSans Font Family */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'TheSans';
|
font-family: "TheSans";
|
||||||
src: url('./assets/TheSansB-W5Plain.woff2') format('woff2');
|
src: url("./assets/TheSansB-W5Plain.woff2") format("woff2");
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'TheSans';
|
font-family: "TheSans";
|
||||||
src: url('./assets/TheSansB-W5PlainItalic.woff2') format('woff2');
|
src: url("./assets/TheSansB-W5PlainItalic.woff2") format("woff2");
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'TheSans Light';
|
font-family: "TheSans Light";
|
||||||
src: url('./assets/TheSansB-W3Light.woff2') format('woff2');
|
src: url("./assets/TheSansB-W3Light.woff2") format("woff2");
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'TheSans Light';
|
font-family: "TheSans Light";
|
||||||
src: url('./assets/TheSansB-W3LightItalic.woff2') format('woff2');
|
src: url("./assets/TheSansB-W3LightItalic.woff2") format("woff2");
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'TheSans SemiLight';
|
font-family: "TheSans SemiLight";
|
||||||
src: url('./assets/TheSansB-W4SemiLight.woff2') format('woff2');
|
src: url("./assets/TheSansB-W4SemiLight.woff2") format("woff2");
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'TheSans SemiLight';
|
font-family: "TheSans SemiLight";
|
||||||
src: url('./assets/TheSansB-W4SemiLightItalic.woff2') format('woff2');
|
src: url("./assets/TheSansB-W4SemiLightItalic.woff2") format("woff2");
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'TheSans Plain';
|
font-family: "TheSans Plain";
|
||||||
src: url('./assets/TheSansB-W5Plain.woff2') format('woff2');
|
src: url("./assets/TheSansB-W5Plain.woff2") format("woff2");
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'TheSans Plain';
|
font-family: "TheSans Plain";
|
||||||
src: url('./assets/TheSansB-W5PlainItalic.woff2') format('woff2');
|
src: url("./assets/TheSansB-W5PlainItalic.woff2") format("woff2");
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'TheSans SemiBold';
|
font-family: "TheSans SemiBold";
|
||||||
src: url('./assets/TheSansB-W6SemiBold.woff2') format('woff2');
|
src: url("./assets/TheSansB-W6SemiBold.woff2") format("woff2");
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'TheSans SemiBold';
|
font-family: "TheSans SemiBold";
|
||||||
src: url('./assets/TheSansB-W6SemiBoldItalic.woff2') format('woff2');
|
src: url("./assets/TheSansB-W6SemiBoldItalic.woff2") format("woff2");
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* Colors */
|
/* Colors */
|
||||||
--color-primary: #05305D;
|
--color-primary: #05305d;
|
||||||
--color-base-canvas: #FFFFFF;
|
--color-base-canvas: #ffffff;
|
||||||
--color-secondary: #34587F;
|
--color-secondary: #34587f;
|
||||||
--color-sky-100: #B0DEE4;
|
--color-sky-100: #b0dee4;
|
||||||
--color-sky-70: #C7E8ED;
|
--color-sky-70: #c7e8ed;
|
||||||
--color-sky-35: #E4F4F7;
|
--color-sky-35: #e4f4f7;
|
||||||
--color-sky-20: #EFF9FA;
|
--color-sky-20: #eff9fa;
|
||||||
--color-base-ink-strong: #4B4B4B;
|
--color-base-ink-strong: #4b4b4b;
|
||||||
--color-base-ink-medium: #BABABA;
|
--color-base-ink-medium: #bababa;
|
||||||
--color-base-ink-soft: #DADADA;
|
--color-base-ink-soft: #dadada;
|
||||||
--color-base-ink-placeholder: #757575;
|
--color-base-ink-placeholder: #757575;
|
||||||
--color-other-red-100: #AA1227;
|
--color-other-red-100: #aa1227;
|
||||||
--color-other-red-10: #F6E6E8;
|
--color-other-red-10: #f6e6e8;
|
||||||
--color-other-green: #539848;
|
--color-other-green: #539848;
|
||||||
--color-su-white: #FFFFFF;
|
--color-su-white: #ffffff;
|
||||||
--color-fire-100: #EB7124;
|
--color-fire-100: #eb7124;
|
||||||
--color-fire-70: #F19B66;
|
--color-fire-70: #f19b66;
|
||||||
--color-fire-35: #F8CDB4;
|
--color-fire-35: #f8cdb4;
|
||||||
--color-fire-20: #FBE2D3;
|
--color-fire-20: #fbe2d3;
|
||||||
|
|
||||||
/* Font sizes */
|
/* Font sizes */
|
||||||
--font-size-body-md: 16px;
|
--font-size-body-md: 16px;
|
||||||
@ -134,26 +134,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--color-primary: #FFFFFF;
|
--color-primary: #ffffff;
|
||||||
--color-base-canvas: #000000;
|
--color-base-canvas: #000000;
|
||||||
--color-secondary: #D9D6D6;
|
--color-secondary: #d9d6d6;
|
||||||
--color-sky-100: #403D3D;
|
--color-sky-100: #403d3d;
|
||||||
--color-sky-70: #2D2B2B;
|
--color-sky-70: #2d2b2b;
|
||||||
--color-sky-35: #1F1E1E;
|
--color-sky-35: #1f1e1e;
|
||||||
--color-sky-20: #141414;
|
--color-sky-20: #141414;
|
||||||
--color-base-ink-strong: #FFFFFF;
|
--color-base-ink-strong: #ffffff;
|
||||||
--color-base-ink-medium: #636363;
|
--color-base-ink-medium: #636363;
|
||||||
--color-base-ink-soft: #555555;
|
--color-base-ink-soft: #555555;
|
||||||
--color-base-ink-placeholder: #959595;
|
--color-base-ink-placeholder: #959595;
|
||||||
--color-other-red-100: #AA1227;
|
--color-other-red-100: #aa1227;
|
||||||
--color-other-red-10: #F6E6E8;
|
--color-other-red-10: #f6e6e8;
|
||||||
--color-other-green: #539848;
|
--color-other-green: #539848;
|
||||||
--color-su-white: #FFFFFF;
|
--color-su-white: #ffffff;
|
||||||
--color-fire-100: #EB7124;
|
--color-fire-100: #eb7124;
|
||||||
--color-fire-70: #F19B66;
|
--color-fire-70: #f19b66;
|
||||||
--color-fire-35: #F8CDB4;
|
--color-fire-35: #f8cdb4;
|
||||||
--color-fire-20: #FBE2D3;
|
--color-fire-20: #fbe2d3;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@ -167,43 +166,43 @@
|
|||||||
|
|
||||||
/* Text styles - Body */
|
/* Text styles - Body */
|
||||||
.body-light-sm {
|
.body-light-sm {
|
||||||
font-family: 'TheSans Light', 'TheSans', system-ui, sans-serif;
|
font-family: "TheSans Light", "TheSans", system-ui, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-normal-md {
|
.body-normal-md {
|
||||||
font-family: 'TheSans SemiLight', 'TheSans', system-ui, sans-serif;
|
font-family: "TheSans SemiLight", "TheSans", system-ui, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-normal-lg {
|
.body-normal-lg {
|
||||||
font-family: 'TheSans SemiLight', 'TheSans', system-ui, sans-serif;
|
font-family: "TheSans SemiLight", "TheSans", system-ui, sans-serif;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-semibold-md {
|
.body-semibold-md {
|
||||||
font-family: 'TheSans Plain', 'TheSans', system-ui, sans-serif;
|
font-family: "TheSans Plain", "TheSans", system-ui, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-semibold-lg {
|
.body-semibold-lg {
|
||||||
font-family: 'TheSans Plain', 'TheSans', system-ui, sans-serif;
|
font-family: "TheSans Plain", "TheSans", system-ui, sans-serif;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-bold-md {
|
.body-bold-md {
|
||||||
font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif;
|
font-family: "TheSans SemiBold", "TheSans", system-ui, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-bold-lg {
|
.body-bold-lg {
|
||||||
font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif;
|
font-family: "TheSans SemiBold", "TheSans", system-ui, sans-serif;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Text styles - Heading */
|
/* Text styles - Heading */
|
||||||
.heading-semibold-lg {
|
.heading-semibold-lg {
|
||||||
font-family: 'TheSans SemiBold', 'TheSans', system-ui, sans-serif;
|
font-family: "TheSans SemiBold", "TheSans", system-ui, sans-serif;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
This is unwieldy. Any way to break it down or group them somehow? Like the border styles, focus styles, colors, and so on.
Is it necessary to repeat the prefix or can you do something like
hover:[a, b, c]instead ofhover:a hover:b hover:c?