fix: harden kumo base component styling

This commit is contained in:
2026-07-02 10:21:25 -07:00
parent 04f2dfc229
commit c394d85236
9 changed files with 234 additions and 13 deletions

View File

@@ -1,6 +1,8 @@
@import "tailwindcss"; @import "tailwindcss";
@import "@cloudflare/kumo/styles"; @import "@cloudflare/kumo/styles";
@source "../node_modules/@cloudflare/kumo/dist";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme inline { @theme inline {
@@ -138,6 +140,207 @@ a,
z-index: 40; z-index: 40;
} }
.teatea-button {
min-height: 2.5rem;
border-radius: var(--radius-md);
font-weight: 600;
transition:
background-color 140ms ease,
border-color 140ms ease,
color 140ms ease,
box-shadow 140ms ease,
opacity 140ms ease;
}
.teatea-button:focus-visible,
.teatea-control:focus-visible,
:where([data-kumo-component="Select"][data-kumo-part="trigger"]):focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring) 24%, transparent);
}
.teatea-button-default {
border: 1px solid var(--primary);
background: var(--primary);
color: var(--primary-foreground);
}
.teatea-button-default:hover {
background: color-mix(in oklch, var(--primary) 88%, black);
border-color: color-mix(in oklch, var(--primary) 88%, black);
}
.teatea-button-outline,
.teatea-button-secondary {
border: 1px solid var(--border);
background: var(--card);
color: var(--foreground);
}
.teatea-button-outline:hover,
.teatea-button-secondary:hover,
.teatea-button-ghost:hover {
border-color: color-mix(in oklch, var(--primary) 45%, var(--border));
background: color-mix(in oklch, var(--secondary) 72%, white);
color: var(--accent-foreground);
}
.teatea-button-ghost {
border: 1px solid transparent;
background: transparent;
color: var(--foreground);
}
.teatea-button-destructive {
border: 1px solid var(--destructive);
background: var(--destructive);
color: var(--destructive-foreground);
}
.teatea-control,
:where([data-kumo-component="Select"][data-kumo-part="trigger"]) {
min-height: 2.5rem;
width: 100%;
border: 1px solid var(--input);
border-radius: var(--radius-md);
background: var(--card);
color: var(--foreground);
font-size: 0.875rem;
font-weight: 500;
box-shadow: 0 1px 2px color-mix(in oklch, var(--foreground) 7%, transparent);
transition:
background-color 140ms ease,
border-color 140ms ease,
box-shadow 140ms ease;
}
.teatea-control {
padding: 0.5rem 0.75rem;
}
:where([data-kumo-component="Select"][data-kumo-part="trigger"]) {
appearance: none;
outline: none;
padding: 0.5rem 0.75rem;
text-align: left;
}
.teatea-control:hover,
:where([data-kumo-component="Select"][data-kumo-part="trigger"]):hover {
border-color: color-mix(in oklch, var(--primary) 45%, var(--input));
background: color-mix(in oklch, var(--secondary) 34%, white);
}
.teatea-control::placeholder {
color: var(--muted-foreground);
}
.teatea-control:disabled,
.teatea-button:disabled,
:where([data-kumo-component="Select"][data-kumo-part="trigger"][data-disabled]) {
cursor: not-allowed;
opacity: 0.55;
}
.teatea-select :where(svg),
:where([data-kumo-component="Select"][data-kumo-part="trigger"]) :where(svg) {
color: var(--primary);
}
:where([role="listbox"]:has([data-kumo-component="Select"][data-kumo-part="option"])) {
z-index: 60;
outline: none;
max-height: min(18rem, var(--available-height, 18rem));
min-width: var(--anchor-width, 12rem);
overflow-y: auto;
overscroll-behavior: contain;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--card);
color: var(--foreground);
padding: 0.375rem;
box-shadow:
0 18px 44px color-mix(in oklch, var(--foreground) 14%, transparent),
0 0 0 1px color-mix(in oklch, var(--primary) 10%, transparent);
}
:where([data-kumo-component="Select"][data-kumo-part="option"]) {
min-height: 2.25rem;
cursor: pointer;
outline: none;
border-radius: calc(var(--radius-md) - 2px);
padding: 0.5rem 0.625rem;
color: var(--foreground);
font-size: 0.875rem;
}
:where([data-kumo-component="Select"][data-kumo-part="option"][data-highlighted]),
:where([data-kumo-component="Select"][data-kumo-part="option"]):hover {
background: var(--secondary);
color: var(--secondary-foreground);
}
:where([data-kumo-component="Select"][data-kumo-part="option"][data-selected]) {
color: var(--primary);
font-weight: 600;
}
.teatea-checkbox :where([data-kumo-component="Checkbox"]) {
border: 1px solid var(--input);
border-radius: var(--radius-sm);
background: var(--card);
color: var(--primary-foreground);
}
.teatea-badge {
border-radius: var(--radius-sm);
font-weight: 600;
}
.teatea-card {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--card);
color: var(--card-foreground);
box-shadow:
0 1px 2px color-mix(in oklch, var(--foreground) 6%, transparent),
0 0 0 1px color-mix(in oklch, var(--primary) 4%, transparent);
}
.teatea-card-header {
background: color-mix(in oklch, var(--secondary) 18%, var(--card));
}
.teatea-table {
width: 100%;
border-collapse: collapse;
color: var(--foreground);
}
.teatea-table :where(thead) {
border-bottom: 1px solid var(--border);
background: var(--secondary);
}
.teatea-table :where(th) {
color: var(--muted-foreground);
font-weight: 600;
}
.teatea-table :where(tbody tr:hover) {
background: color-mix(in oklch, var(--secondary) 42%, transparent);
}
.teatea-dialog {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--card);
color: var(--foreground);
box-shadow:
0 26px 80px color-mix(in oklch, var(--foreground) 16%, transparent),
0 0 0 1px color-mix(in oklch, var(--primary) 10%, transparent);
}
.loading-progress { .loading-progress {
width: 55%; width: 55%;
animation: loading-progress 900ms ease-in-out infinite; animation: loading-progress 900ms ease-in-out infinite;

View File

@@ -1,6 +1,8 @@
import * as React from "react"; import * as React from "react";
import { Badge as KumoBadge } from "@cloudflare/kumo/components/badge"; import { Badge as KumoBadge } from "@cloudflare/kumo/components/badge";
import { cn } from "@/lib/utils";
type KumoBadgeProps = React.ComponentProps<typeof KumoBadge>; type KumoBadgeProps = React.ComponentProps<typeof KumoBadge>;
type LegacyBadgeVariant = "default" | "secondary" | "outline" | "success" | "warning" | "danger"; type LegacyBadgeVariant = "default" | "secondary" | "outline" | "success" | "warning" | "danger";
@@ -19,8 +21,8 @@ export type BadgeProps = Omit<KumoBadgeProps, "variant" | "children"> & {
children?: React.ReactNode; children?: React.ReactNode;
}; };
export function Badge({ variant = "default", ...props }: BadgeProps): React.ReactElement { export function Badge({ className, variant = "default", ...props }: BadgeProps): React.ReactElement {
const mappedVariant = variant in variantMap ? variantMap[variant as LegacyBadgeVariant] : (variant as KumoBadgeProps["variant"]); const mappedVariant = variant in variantMap ? variantMap[variant as LegacyBadgeVariant] : (variant as KumoBadgeProps["variant"]);
return <KumoBadge variant={mappedVariant} {...(props as KumoBadgeProps)} />; return <KumoBadge className={cn("teatea-badge", className)} variant={mappedVariant} {...(props as KumoBadgeProps)} />;
} }

View File

@@ -6,6 +6,8 @@ import {
type LinkButtonProps as KumoLinkButtonProps, type LinkButtonProps as KumoLinkButtonProps,
} from "@cloudflare/kumo/components/button"; } from "@cloudflare/kumo/components/button";
import { cn } from "@/lib/utils";
type ButtonVariant = "default" | "secondary" | "outline" | "ghost" | "destructive"; type ButtonVariant = "default" | "secondary" | "outline" | "ghost" | "destructive";
type ButtonSize = "default" | "sm" | "lg" | "icon"; type ButtonSize = "default" | "sm" | "lg" | "icon";
@@ -35,10 +37,11 @@ export type LinkButtonProps = Omit<KumoLinkButtonProps, "variant" | "size"> & {
}; };
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button( export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ variant = "default", size = "default", ...props }, { className, variant = "default", size = "default", ...props },
ref, ref,
): React.ReactElement { ): React.ReactElement {
const kumoProps = { const kumoProps = {
className: cn("teatea-button", `teatea-button-${variant}`, className),
ref, ref,
shape: size === "icon" ? "square" : "base", shape: size === "icon" ? "square" : "base",
size: sizeMap[size], size: sizeMap[size],
@@ -52,8 +55,16 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function
}); });
export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkButtonProps>(function LinkButton( export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkButtonProps>(function LinkButton(
{ variant = "default", size = "default", ...props }, { className, variant = "default", size = "default", ...props },
ref, ref,
): React.ReactElement { ): React.ReactElement {
return <KumoLinkButton ref={ref} size={sizeMap[size]} variant={variantMap[variant]} {...props} />; return (
<KumoLinkButton
ref={ref}
className={cn("teatea-button", `teatea-button-${variant}`, className)}
size={sizeMap[size]}
variant={variantMap[variant]}
{...props}
/>
);
}); });

View File

@@ -6,7 +6,7 @@ export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElemen
return ( return (
<div <div
className={cn( className={cn(
"overflow-hidden rounded-lg border border-kumo-line bg-kumo-base text-card-foreground shadow-sm", "teatea-card overflow-hidden rounded-lg border border-kumo-line bg-kumo-base text-card-foreground shadow-sm",
className, className,
)} )}
{...props} {...props}
@@ -15,7 +15,7 @@ export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElemen
} }
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): React.ReactElement { export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
return <div className={cn("flex flex-col gap-1.5 bg-kumo-elevated p-5", className)} {...props} />; return <div className={cn("teatea-card-header flex flex-col gap-1.5 bg-kumo-elevated p-5", className)} {...props} />;
} }
export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>): React.ReactElement { export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>): React.ReactElement {

View File

@@ -3,6 +3,8 @@
import * as React from "react"; import * as React from "react";
import { Checkbox as KumoCheckbox, type CheckboxProps as KumoCheckboxProps } from "@cloudflare/kumo/components/checkbox"; import { Checkbox as KumoCheckbox, type CheckboxProps as KumoCheckboxProps } from "@cloudflare/kumo/components/checkbox";
import { cn } from "@/lib/utils";
type CheckboxProps = Omit<KumoCheckboxProps, "checked" | "onCheckedChange"> & { type CheckboxProps = Omit<KumoCheckboxProps, "checked" | "onCheckedChange"> & {
checked?: boolean; checked?: boolean;
defaultChecked?: boolean; defaultChecked?: boolean;
@@ -11,6 +13,7 @@ type CheckboxProps = Omit<KumoCheckboxProps, "checked" | "onCheckedChange"> & {
export function Checkbox({ export function Checkbox({
checked, checked,
className,
defaultChecked = false, defaultChecked = false,
name, name,
onCheckedChange, onCheckedChange,
@@ -21,9 +24,10 @@ export function Checkbox({
const currentChecked = isControlled ? checked : uncontrolledChecked; const currentChecked = isControlled ? checked : uncontrolledChecked;
return ( return (
<> <span className="teatea-checkbox contents">
<KumoCheckbox <KumoCheckbox
checked={currentChecked} checked={currentChecked}
className={cn("teatea-checkbox-control", className)}
onCheckedChange={(nextChecked) => { onCheckedChange={(nextChecked) => {
const booleanValue = Boolean(nextChecked); const booleanValue = Boolean(nextChecked);
if (!isControlled) { if (!isControlled) {
@@ -34,6 +38,6 @@ export function Checkbox({
{...props} {...props}
/> />
{name && currentChecked ? <input name={name} type="hidden" value="on" /> : null} {name && currentChecked ? <input name={name} type="hidden" value="on" /> : null}
</> </span>
); );
} }

View File

@@ -40,7 +40,7 @@ export function Dialog({
> >
<KumoDialog <KumoDialog
className={cn( className={cn(
"dialog-panel-enter fixed left-1/2 top-1/2 z-50 flex max-h-[min(92svh,52rem)] w-[min(calc(100vw-1.5rem),48rem)] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-kumo-line bg-kumo-base p-0 shadow-xl outline-none ring-1 ring-kumo-line", "teatea-dialog dialog-panel-enter fixed left-1/2 top-1/2 z-50 flex max-h-[min(92svh,52rem)] w-[min(calc(100vw-1.5rem),48rem)] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-kumo-line bg-kumo-base p-0 shadow-xl outline-none ring-1 ring-kumo-line",
"data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0", "data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0",
className, className,
)} )}

View File

@@ -17,12 +17,12 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(function Inp
{ className, ...props }, { className, ...props },
ref, ref,
): React.ReactElement { ): React.ReactElement {
return <KumoInput ref={ref} className={cn("w-full", className)} {...props} />; return <KumoInput ref={ref} className={cn("teatea-control w-full", className)} {...props} />;
}); });
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea( export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
{ className, ...props }, { className, ...props },
ref, ref,
): React.ReactElement { ): React.ReactElement {
return <KumoTextarea ref={ref} className={cn("min-h-24 w-full", className)} {...props} />; return <KumoTextarea ref={ref} className={cn("teatea-control min-h-24 w-full", className)} {...props} />;
}); });

View File

@@ -37,6 +37,7 @@ export function Select({
return ( return (
<KumoSelect<string> <KumoSelect<string>
className={cn( className={cn(
"teatea-select",
"min-h-10 w-full rounded-md border border-input bg-card px-3 py-2 text-left text-sm font-medium text-foreground shadow-sm", "min-h-10 w-full rounded-md border border-input bg-card px-3 py-2 text-left text-sm font-medium text-foreground shadow-sm",
"transition-colors hover:border-primary/45 hover:bg-secondary/35", "transition-colors hover:border-primary/45 hover:bg-secondary/35",
"focus-visible:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/25", "focus-visible:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/25",

View File

@@ -47,7 +47,7 @@ const TableRoot = React.forwardRef<HTMLTableElement, TableProps>(function Table(
return ( return (
<table <table
ref={ref} ref={ref}
className={cn("border-collapse text-kumo-default", layout === "fixed" && "table-fixed", className)} className={cn("teatea-table border-collapse text-kumo-default", layout === "fixed" && "table-fixed", className)}
{...props} {...props}
/> />
); );