fix(frontend): stabilize multiselect behavior and UI spacing (#154)

* fix(frontend): left-align announcement logo in homepage banner

* fix(frontend): keep multiselect panels floating and preserve summaries

* fix(frontend): rebalance config card header spacing

* fix(frontend): normalize formatting and fix multiselect modal behavior

* fix(frontend): restore config divider spacing on desktop
This commit is contained in:
sagit
2026-02-19 20:50:12 +08:00
committed by GitHub
parent 18c2da7c7e
commit 66de566a00
52 changed files with 743 additions and 301 deletions
@@ -9,7 +9,8 @@ import {
type AlertColor = "default" | "success" | "warning" | "danger" | "primary";
type AlertVariant = "solid" | "flat" | "faded" | "bordered";
interface AlertProps extends Omit<React.ComponentProps<"div">, "color" | "title"> {
interface AlertProps
extends Omit<React.ComponentProps<"div">, "color" | "title"> {
color?: AlertColor;
description?: React.ReactNode;
title?: React.ReactNode;
@@ -4,8 +4,20 @@ import { Loader2Icon } from "lucide-react";
import { Button as BaseButton } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type HeroButtonColor = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
type HeroButtonVariant = "solid" | "light" | "flat" | "ghost" | "bordered" | "shadow";
type HeroButtonColor =
| "default"
| "primary"
| "secondary"
| "success"
| "warning"
| "danger";
type HeroButtonVariant =
| "solid"
| "light"
| "flat"
| "ghost"
| "bordered"
| "shadow";
type HeroButtonSize = "sm" | "md" | "lg";
export interface ButtonProps
@@ -20,7 +32,17 @@ export interface ButtonProps
variant?: HeroButtonVariant;
}
function mapVariant(color: HeroButtonColor, variant: HeroButtonVariant): "default" | "destructive" | "secondary" | "outline" | "ghost" | "light" | "flat" {
function mapVariant(
color: HeroButtonColor,
variant: HeroButtonVariant,
):
| "default"
| "destructive"
| "secondary"
| "outline"
| "ghost"
| "light"
| "flat" {
if (variant === "bordered") {
return "outline";
}
@@ -43,7 +65,10 @@ function mapVariant(color: HeroButtonColor, variant: HeroButtonVariant): "defaul
return "default";
}
function mapSize(size: HeroButtonSize, isIconOnly: boolean): "default" | "sm" | "lg" | "icon" {
function mapSize(
size: HeroButtonSize,
isIconOnly: boolean,
): "default" | "sm" | "lg" | "icon" {
if (isIconOnly) {
return "icon";
}
@@ -67,33 +92,46 @@ const solidColorClassMap: Partial<Record<HeroButtonColor, string>> = {
const borderedColorClassMap: Record<HeroButtonColor, string> = {
default:
"border-default-300 text-default-700 hover:bg-default-100 dark:border-default-300 dark:text-default-600 dark:hover:bg-default-200/40",
primary: "border-primary text-primary hover:bg-primary-50 dark:border-primary-500/60 dark:text-primary-300 dark:hover:bg-primary-900/20",
primary:
"border-primary text-primary hover:bg-primary-50 dark:border-primary-500/60 dark:text-primary-300 dark:hover:bg-primary-900/20",
secondary:
"border-secondary text-secondary hover:bg-secondary-50 dark:border-secondary-500/60 dark:text-secondary-300 dark:hover:bg-secondary-900/20",
success: "border-success text-success hover:bg-success-50 dark:border-success-500/60 dark:text-success-300 dark:hover:bg-success-900/20",
success:
"border-success text-success hover:bg-success-50 dark:border-success-500/60 dark:text-success-300 dark:hover:bg-success-900/20",
warning:
"border-warning text-warning-700 hover:bg-warning-50 dark:border-warning-500/60 dark:text-warning-300 dark:hover:bg-warning-900/20",
danger: "border-danger text-danger hover:bg-danger-50 dark:border-danger-500/60 dark:text-danger-300 dark:hover:bg-danger-900/20",
danger:
"border-danger text-danger hover:bg-danger-50 dark:border-danger-500/60 dark:text-danger-300 dark:hover:bg-danger-900/20",
};
const lightColorClassMap: Record<HeroButtonColor, string> = {
default: "text-default-700 hover:bg-default-100 dark:text-default-600 dark:hover:bg-default-200/40",
primary: "text-primary hover:bg-primary-100/70 dark:text-primary-300 dark:hover:bg-primary-900/30",
secondary: "text-secondary hover:bg-secondary-100/70 dark:text-secondary-300 dark:hover:bg-secondary-900/30",
success: "text-success hover:bg-success-100/70 dark:text-success-300 dark:hover:bg-success-900/30",
warning: "text-warning-700 hover:bg-warning-100/70 dark:text-warning-300 dark:hover:bg-warning-900/30",
danger: "text-danger hover:bg-danger-100/70 dark:text-danger-300 dark:hover:bg-danger-900/30",
default:
"text-default-700 hover:bg-default-100 dark:text-default-600 dark:hover:bg-default-200/40",
primary:
"text-primary hover:bg-primary-100/70 dark:text-primary-300 dark:hover:bg-primary-900/30",
secondary:
"text-secondary hover:bg-secondary-100/70 dark:text-secondary-300 dark:hover:bg-secondary-900/30",
success:
"text-success hover:bg-success-100/70 dark:text-success-300 dark:hover:bg-success-900/30",
warning:
"text-warning-700 hover:bg-warning-100/70 dark:text-warning-300 dark:hover:bg-warning-900/30",
danger:
"text-danger hover:bg-danger-100/70 dark:text-danger-300 dark:hover:bg-danger-900/30",
};
const flatColorClassMap: Record<HeroButtonColor, string> = {
default:
"bg-default-100 text-default-700 hover:bg-default-200 dark:bg-default-100/45 dark:text-default-700 dark:hover:bg-default-200/60",
primary: "bg-primary-100 text-primary-700 hover:bg-primary-200 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/45",
primary:
"bg-primary-100 text-primary-700 hover:bg-primary-200 dark:bg-primary-900/30 dark:text-primary-300 dark:hover:bg-primary-900/45",
secondary:
"bg-secondary-100 text-secondary-700 hover:bg-secondary-200 dark:bg-secondary-900/30 dark:text-secondary-300 dark:hover:bg-secondary-900/45",
success: "bg-success-100 text-success-700 hover:bg-success-200 dark:bg-success-900/30 dark:text-success-300 dark:hover:bg-success-900/45",
warning: "bg-warning-100 text-warning-700 hover:bg-warning-200 dark:bg-warning-900/30 dark:text-warning-300 dark:hover:bg-warning-900/45",
danger: "bg-danger-100 text-danger-700 hover:bg-danger-200 dark:bg-danger-900/30 dark:text-danger-300 dark:hover:bg-danger-900/45",
success:
"bg-success-100 text-success-700 hover:bg-success-200 dark:bg-success-900/30 dark:text-success-300 dark:hover:bg-success-900/45",
warning:
"bg-warning-100 text-warning-700 hover:bg-warning-200 dark:bg-warning-900/30 dark:text-warning-300 dark:hover:bg-warning-900/45",
danger:
"bg-danger-100 text-danger-700 hover:bg-danger-200 dark:bg-danger-900/30 dark:text-danger-300 dark:hover:bg-danger-900/45",
};
const shadowColorClassMap: Record<HeroButtonColor, string> = {
@@ -105,7 +143,10 @@ const shadowColorClassMap: Record<HeroButtonColor, string> = {
danger: "shadow-md shadow-danger-500/35",
};
function mapColorClass(color: HeroButtonColor, variant: HeroButtonVariant): string {
function mapColorClass(
color: HeroButtonColor,
variant: HeroButtonVariant,
): string {
if (variant === "bordered") {
return borderedColorClassMap[color];
}
@@ -122,7 +163,10 @@ function mapColorClass(color: HeroButtonColor, variant: HeroButtonVariant): stri
return "";
}
function mapShadowClass(color: HeroButtonColor, variant: HeroButtonVariant): string {
function mapShadowClass(
color: HeroButtonColor,
variant: HeroButtonVariant,
): string {
if (variant !== "shadow") {
return "";
}
@@ -169,16 +213,23 @@ export const Button = React.forwardRef<
return (
<BaseButton
className={cn(isIconOnly ? "p-0" : "", resolvedColorClass, resolvedShadowClass, className)}
disabled={resolvedDisabled}
ref={ref}
className={cn(
isIconOnly ? "p-0" : "",
resolvedColorClass,
resolvedShadowClass,
className,
)}
disabled={resolvedDisabled}
size={resolvedSize}
type={type}
variant={resolvedVariant}
onClick={handleClick}
{...props}
>
{isLoading ? <Loader2Icon className="mr-2 h-4 w-4 animate-spin" /> : null}
{isLoading ? (
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
) : null}
{startContent}
{isIconOnly ? null : children}
{isIconOnly ? children : null}
@@ -11,10 +11,18 @@ export function Card({ className, ...props }: React.ComponentProps<"div">) {
return <BaseCard className={className} {...props} />;
}
export function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
export function CardHeader({
className,
...props
}: React.ComponentProps<"div">) {
return <BaseCardHeader className={cn("p-4 md:p-6", className)} {...props} />;
}
export function CardBody({ className, ...props }: React.ComponentProps<"div">) {
return <CardContent className={cn("p-4 pt-0 md:p-6 md:pt-0", className)} {...props} />;
return (
<CardContent
className={cn("p-4 pt-0 md:p-6 md:pt-0", className)}
{...props}
/>
);
}
@@ -4,7 +4,10 @@ import { Checkbox as BaseCheckbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
export interface CheckboxProps
extends Omit<React.ComponentProps<typeof BaseCheckbox>, "checked" | "onCheckedChange"> {
extends Omit<
React.ComponentProps<typeof BaseCheckbox>,
"checked" | "onCheckedChange"
> {
classNames?: Record<string, string>;
color?: string;
isDisabled?: boolean;
@@ -30,7 +33,13 @@ export function Checkbox({
};
return (
<div className={cn("inline-flex items-center gap-2", isDisabled ? "opacity-50" : "", className)}>
<div
className={cn(
"inline-flex items-center gap-2",
isDisabled ? "opacity-50" : "",
className,
)}
>
<BaseCheckbox
checked={Boolean(isSelected)}
disabled={isDisabled}
@@ -3,7 +3,13 @@ import * as React from "react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
type ChipColor = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
type ChipColor =
| "default"
| "primary"
| "secondary"
| "success"
| "warning"
| "danger";
type ChipVariant = "solid" | "flat" | "light" | "bordered";
type ChipSize = "sm" | "md" | "lg";
@@ -1,10 +1,10 @@
import * as React from "react";
import { FieldContainer, type FieldMetaProps } from "./shared";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { FieldContainer, type FieldMetaProps } from "./shared";
interface CalendarDateLike {
day: number;
month: number;
@@ -76,7 +76,11 @@ export function DatePicker({
const month = Number(monthText);
const day = Number(dayText);
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
if (
!Number.isFinite(year) ||
!Number.isFinite(month) ||
!Number.isFinite(day)
) {
onChange(null);
return;
@@ -8,7 +8,6 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger as BaseDropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
type Placement = "bottom-start" | "bottom-end" | "top-start" | "top-end";
@@ -17,14 +16,19 @@ interface DropdownContextValue {
align: "start" | "end";
}
const DropdownContext = React.createContext<DropdownContextValue>({ align: "start" });
const DropdownContext = React.createContext<DropdownContextValue>({
align: "start",
});
export interface DropdownProps {
children: React.ReactNode;
placement?: Placement;
}
export function Dropdown({ children, placement = "bottom-start" }: DropdownProps) {
export function Dropdown({
children,
placement = "bottom-start",
}: DropdownProps) {
const align = placement.endsWith("end") ? "end" : "start";
return (
@@ -66,7 +70,10 @@ export function DropdownItem({
}: DropdownItemProps) {
return (
<BaseDropdownMenuItem
className={cn(color === "danger" ? "text-danger focus:text-danger" : "", className)}
className={cn(
color === "danger" ? "text-danger focus:text-danger" : "",
className,
)}
onSelect={(event) => {
event.preventDefault();
onPress?.();
@@ -1,11 +1,11 @@
import * as React from "react";
import { FieldContainer, type FieldMetaProps } from "./shared";
import { Input as BaseInput } from "@/components/ui/input";
import { Textarea as BaseTextarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { FieldContainer, type FieldMetaProps } from "./shared";
type ClassNameMap = {
base?: string;
description?: string;
@@ -84,7 +84,9 @@ export function Input({
classNames?.inputWrapper,
)}
>
{startContent ? <div className="pl-3 text-default-500">{startContent}</div> : null}
{startContent ? (
<div className="pl-3 text-default-500">{startContent}</div>
) : null}
<BaseInput
aria-invalid={isInvalid}
className={cn(
@@ -100,7 +102,9 @@ export function Input({
required={isRequired}
{...props}
/>
{endContent ? <div className="pr-3 text-default-500">{endContent}</div> : null}
{endContent ? (
<div className="pr-3 text-default-500">{endContent}</div>
) : null}
</div>
</FieldContainer>
);
@@ -20,5 +20,10 @@ function mapColor(color: LinkColor) {
}
export function Link({ className, color = "default", ...props }: LinkProps) {
return <a className={cn("transition-colors", mapColor(color), className)} {...props} />;
return (
<a
className={cn("transition-colors", mapColor(color), className)}
{...props}
/>
);
}
@@ -52,7 +52,12 @@ export function Navbar({
style={height ? { minHeight: height } : undefined}
{...props}
>
<div className={cn("mx-auto flex h-full w-full items-center justify-between px-4", maxWidthClass(maxWidth))}>
<div
className={cn(
"mx-auto flex h-full w-full items-center justify-between px-4",
maxWidthClass(maxWidth),
)}
>
{children}
</div>
</nav>
@@ -85,6 +90,9 @@ export function NavbarContent({
);
}
export function NavbarBrand({ className, ...props }: React.ComponentProps<"div">) {
export function NavbarBrand({
className,
...props
}: React.ComponentProps<"div">) {
return <div className={cn("flex items-center", className)} {...props} />;
}
@@ -3,7 +3,13 @@ import * as React from "react";
import { Progress as BaseProgress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
type ProgressColor = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
type ProgressColor =
| "default"
| "primary"
| "secondary"
| "success"
| "warning"
| "danger";
export interface ProgressProps {
"aria-label"?: string;
@@ -1,6 +1,9 @@
import * as React from "react";
import { RadioGroup as BaseRadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
RadioGroup as BaseRadioGroup,
RadioGroupItem,
} from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
@@ -8,7 +11,9 @@ interface RadioContextValue {
name: string;
}
const RadioContext = React.createContext<RadioContextValue>({ name: "radio-group" });
const RadioContext = React.createContext<RadioContextValue>({
name: "radio-group",
});
export interface RadioGroupProps {
children: React.ReactNode;
@@ -33,7 +38,9 @@ export function RadioGroup({
<RadioContext.Provider value={{ name: generatedName }}>
<BaseRadioGroup
className={cn(
orientation === "horizontal" ? "flex flex-wrap items-center gap-4" : "grid gap-3",
orientation === "horizontal"
? "flex flex-wrap items-center gap-4"
: "grid gap-3",
)}
value={value}
onValueChange={onValueChange}
+133 -68
View File
@@ -169,19 +169,71 @@ export function Select<T>({
() => getOptions(children, items),
[children, items],
);
const containerRef = React.useRef<HTMLDivElement | null>(null);
const listboxRef = React.useRef<HTMLDivElement | null>(null);
const [isExpanded, setIsExpanded] = React.useState(false);
const selected = React.useMemo(() => toSet(selectedKeys), [selectedKeys]);
const disabled = React.useMemo(() => toSet(disabledKeys), [disabledKeys]);
React.useEffect(() => {
if (!isExpanded) {
return;
}
const handlePointerDown = (event: MouseEvent | TouchEvent) => {
const container = containerRef.current;
const listbox = listboxRef.current;
if (!container) {
return;
}
const target = event.target;
if (!(target instanceof Node)) {
return;
}
if (container.contains(target) || listbox?.contains(target)) {
return;
}
setIsExpanded(false);
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setIsExpanded(false);
}
};
document.addEventListener("mousedown", handlePointerDown);
document.addEventListener("touchstart", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("mousedown", handlePointerDown);
document.removeEventListener("touchstart", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, [isExpanded]);
React.useEffect(() => {
if (isDisabled) {
setIsExpanded(false);
}
}, [isDisabled]);
const selectedArray = Array.from(selected);
const singleValue = selectedArray[0] ?? "";
const selectedLabels = options
.filter((option) => selected.has(option.key))
.map((option) => option.label);
const resolvedSelectedValues =
selectedLabels.length > 0 ? selectedLabels : selectedArray;
const selectedFullText = resolvedSelectedValues.join("、");
const selectedText =
selectedArray.length > 0
? options
.filter((option) => selected.has(option.key))
.map((option) => option.label)
.join("、")
: (placeholder ?? "请选择");
selectedArray.length > 0 ? selectedFullText : (placeholder ?? "请选择");
const updateMultipleSelection = (key: string, checked?: boolean) => {
if (isDisabled || disabled.has(key)) {
@@ -227,6 +279,70 @@ export function Select<T>({
onSelectionChange(new Set([event.target.value]));
};
const renderMultipleListbox = () => {
if (!isExpanded) {
return null;
}
return (
<div
className="absolute left-0 top-full z-50 mt-1 max-h-56 w-full space-y-1 overflow-y-auto rounded-md border border-divider bg-background p-2 shadow-md"
id={`${generatedId}-listbox`}
ref={listboxRef}
role="listbox"
>
{options.length === 0 ? (
<div
className={cn(
"px-2 py-1 text-default-500",
textSizeClass(size),
)}
>
暂无可选项
</div>
) : (
options.map((option) => {
const optionDisabled = isDisabled || disabled.has(option.key);
return (
<div
key={option.key}
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5",
optionDisabled
? "cursor-not-allowed opacity-60"
: "hover:bg-default-100",
)}
>
<BaseCheckbox
checked={selected.has(option.key)}
disabled={optionDisabled}
onCheckedChange={(value) =>
updateMultipleSelection(option.key, value === true)
}
/>
<button
className={cn(
"min-w-0 flex-1 truncate text-left text-foreground",
textSizeClass(size),
optionDisabled
? "cursor-not-allowed"
: "cursor-pointer",
)}
disabled={optionDisabled}
type="button"
onClick={() => updateMultipleSelection(option.key)}
>
{option.label}
</button>
</div>
);
})
)}
</div>
);
};
return (
<FieldContainer
className={classNames?.base}
@@ -239,21 +355,23 @@ export function Select<T>({
>
{selectionMode === "multiple" ? (
<div
ref={containerRef}
className={cn(
"w-full overflow-hidden rounded-md border border-input bg-background shadow-sm",
isDisabled ? "cursor-not-allowed opacity-60" : "",
"relative w-full",
className,
)}
id={generatedId}
>
<button
aria-controls={`${generatedId}-listbox`}
aria-expanded={isExpanded}
aria-haspopup="listbox"
className={cn(
"flex w-full min-w-0 items-center gap-2 overflow-hidden px-3 py-2 text-left",
"flex w-full min-w-0 items-center gap-2 overflow-hidden rounded-md border border-input bg-background px-3 py-2 text-left shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
isDisabled ? "cursor-not-allowed opacity-60" : "",
classNames?.trigger,
)}
disabled={isDisabled}
id={generatedId}
type="button"
onClick={() => setIsExpanded((prev) => !prev)}
>
@@ -261,9 +379,11 @@ export function Select<T>({
className={cn(
"block min-w-0 flex-1 truncate",
textSizeClass(size),
selectedArray.length > 0 ? "text-foreground" : "text-default-500",
selectedArray.length > 0
? "text-foreground"
: "text-default-500",
)}
title={selectedText}
title={selectedArray.length > 0 ? selectedFullText : undefined}
>
{selectedText}
</span>
@@ -274,62 +394,7 @@ export function Select<T>({
)}
/>
</button>
{isExpanded ? (
<div
className="max-h-56 space-y-1 overflow-y-auto border-t border-divider p-2"
id={`${generatedId}-listbox`}
role="listbox"
>
{options.length === 0 ? (
<div
className={cn(
"px-2 py-1 text-default-500",
textSizeClass(size),
)}
>
暂无可选项
</div>
) : (
options.map((option) => {
const optionDisabled = isDisabled || disabled.has(option.key);
return (
<div
key={option.key}
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5",
optionDisabled
? "cursor-not-allowed opacity-60"
: "hover:bg-default-100",
)}
>
<BaseCheckbox
checked={selected.has(option.key)}
disabled={optionDisabled}
onCheckedChange={(value) =>
updateMultipleSelection(option.key, value === true)
}
/>
<button
className={cn(
"min-w-0 flex-1 truncate text-left text-foreground",
textSizeClass(size),
optionDisabled
? "cursor-not-allowed"
: "cursor-pointer",
)}
disabled={optionDisabled}
type="button"
onClick={() => updateMultipleSelection(option.key)}
>
{option.label}
</button>
</div>
);
})
)}
</div>
) : null}
{renderMultipleListbox()}
</div>
) : (
<select
@@ -36,7 +36,9 @@ export function FieldContainer({
</Label>
) : null}
{children}
{description ? <p className="text-xs text-default-500">{description}</p> : null}
{description ? (
<p className="text-xs text-default-500">{description}</p>
) : null}
{isInvalid && errorMessage ? (
<p className="text-xs text-danger">{errorMessage}</p>
) : null}
@@ -45,14 +47,21 @@ export function FieldContainer({
}
export function extractText(content: React.ReactNode): string {
if (content === null || content === undefined || typeof content === "boolean") {
if (
content === null ||
content === undefined ||
typeof content === "boolean"
) {
return "";
}
if (typeof content === "string" || typeof content === "number") {
return String(content);
}
if (Array.isArray(content)) {
return content.map((item) => extractText(item)).join("").trim();
return content
.map((item) => extractText(item))
.join("")
.trim();
}
if (React.isValidElement(content)) {
return extractText(content.props.children);
@@ -21,10 +21,17 @@ function iconSize(size: SpinnerSize | undefined) {
return "h-5 w-5";
}
export function Spinner({ className, label, size = "md", ...props }: SpinnerProps) {
export function Spinner({
className,
label,
size = "md",
...props
}: SpinnerProps) {
return (
<div className={cn("inline-flex items-center gap-2", className)} {...props}>
<Loader2Icon className={cn("animate-spin text-default-500", iconSize(size))} />
<Loader2Icon
className={cn("animate-spin text-default-500", iconSize(size))}
/>
{label ? <span className="text-sm text-default-500">{label}</span> : null}
</div>
);
@@ -3,7 +3,11 @@ import * as React from "react";
import { Switch as BaseSwitch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
export interface SwitchProps extends Omit<React.ComponentProps<typeof BaseSwitch>, "checked" | "onCheckedChange"> {
export interface SwitchProps
extends Omit<
React.ComponentProps<typeof BaseSwitch>,
"checked" | "onCheckedChange"
> {
classNames?: Record<string, string>;
color?: string;
isDisabled?: boolean;
@@ -17,10 +17,17 @@ export interface TableProps extends React.ComponentProps<"table"> {
classNames?: TableClassNames;
}
export function Table({ children, className, classNames, ...props }: TableProps) {
export function Table({
children,
className,
classNames,
...props
}: TableProps) {
return (
<TableStyleContext.Provider value={{ thClassName: classNames?.th }}>
<div className={cn("w-full overflow-auto rounded-md", classNames?.wrapper)}>
<div
className={cn("w-full overflow-auto rounded-md", classNames?.wrapper)}
>
<table className={cn("w-full text-sm", className)} {...props}>
{children}
</table>
@@ -29,7 +36,11 @@ export function Table({ children, className, classNames, ...props }: TableProps)
);
}
export function TableHeader({ children, className, ...props }: React.ComponentProps<"thead">) {
export function TableHeader({
children,
className,
...props
}: React.ComponentProps<"thead">) {
const childArray = React.Children.toArray(children);
const hasRow = childArray.some(
(child) => React.isValidElement(child) && child.type === TableRow,
@@ -42,7 +53,8 @@ export function TableHeader({ children, className, ...props }: React.ComponentPr
);
}
interface TableBodyProps<T> extends Omit<React.ComponentProps<"tbody">, "children"> {
interface TableBodyProps<T>
extends Omit<React.ComponentProps<"tbody">, "children"> {
children?: React.ReactNode | ((item: T) => React.ReactNode);
emptyContent?: React.ReactNode;
isLoading?: boolean;
@@ -120,19 +132,28 @@ export function TableBody<T>({
);
}
export function TableColumn({ className, ...props }: React.ComponentProps<"th">) {
export function TableColumn({
className,
...props
}: React.ComponentProps<"th">) {
const { thClassName } = React.useContext(TableStyleContext);
return (
<th
className={cn("px-3 py-2 text-left font-medium text-default-600", thClassName, className)}
className={cn(
"px-3 py-2 text-left font-medium text-default-600",
thClassName,
className,
)}
{...props}
/>
);
}
export function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return <tr className={cn("border-b last:border-b-0", className)} {...props} />;
return (
<tr className={cn("border-b last:border-b-0", className)} {...props} />
);
}
export function TableCell({ className, ...props }: React.ComponentProps<"td">) {
@@ -10,11 +10,14 @@ function resolveInitialTheme(): ThemeMode {
}
const fromStorage = window.localStorage.getItem(STORAGE_KEY);
if (fromStorage === "dark" || fromStorage === "light") {
return fromStorage;
}
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
let currentTheme: ThemeMode = resolveInitialTheme();