feat: improve admin panel design (#14464)

This commit is contained in:
DarkSky
2026-02-17 17:40:29 +08:00
committed by GitHub
parent 850e646ab9
commit 8f833388eb
86 changed files with 2633 additions and 1431 deletions
@@ -39,7 +39,7 @@ export const ConfirmDialog = ({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:w-[460px]">
<DialogContent className="sm:max-w-[460px]">
<DialogHeader>
<DialogTitle className="leading-7">{title}</DialogTitle>
<DialogDescription className="leading-6">
@@ -48,13 +48,19 @@ export const ConfirmDialog = ({
</DialogHeader>
<DialogFooter className="mt-6">
<div className="flex justify-end gap-2 items-center w-full">
<Button type="button" onClick={handleClose} variant="outline">
<Button
type="button"
onClick={handleClose}
variant="outline"
size="sm"
>
<span>{cancelText}</span>
</Button>
<Button
type="button"
onClick={onConfirm}
variant={confirmButtonVariant}
size="sm"
>
<span>{confirmText}</span>
</Button>
@@ -0,0 +1,85 @@
/**
* @vitest-environment happy-dom
*/
import type { ColumnDef } from '@tanstack/react-table';
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, test, vi } from 'vitest';
import { SharedDataTable } from './data-table';
const { DataTablePaginationMock } = vi.hoisted(() => ({
DataTablePaginationMock: vi.fn(({ disabled }: { disabled?: boolean }) => (
<div data-disabled={disabled ? 'true' : 'false'} data-testid="pagination" />
)),
}));
vi.mock('./data-table-pagination', () => ({
DataTablePagination: DataTablePaginationMock,
}));
type Row = { id: string; name: string };
const columns: ColumnDef<Row>[] = [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => row.original.name,
},
];
describe('SharedDataTable', () => {
afterEach(() => {
cleanup();
DataTablePaginationMock.mockClear();
});
test('renders token-aligned table shell and row data', () => {
const { container } = render(
<SharedDataTable
columns={columns}
data={[{ id: '1', name: 'Alice' }]}
totalCount={1}
pagination={{ pageIndex: 0, pageSize: 10 }}
onPaginationChange={vi.fn()}
/>
);
expect(screen.queryByText('Alice')).not.toBeNull();
const shell = container.querySelector('.rounded-xl');
expect(shell).not.toBeNull();
expect(shell?.className).toContain('border-border');
expect(shell?.className).toContain('bg-card');
expect(shell?.className).toContain('shadow-1');
});
test('shows loading overlay and disables pagination while loading', () => {
render(
<SharedDataTable
columns={columns}
data={[{ id: '1', name: 'Alice' }]}
totalCount={1}
pagination={{ pageIndex: 0, pageSize: 10 }}
onPaginationChange={vi.fn()}
loading={true}
/>
);
expect(screen.queryByText('Loading...')).not.toBeNull();
expect(screen.getByTestId('pagination').dataset.disabled).toBe('true');
});
test('renders empty state when there is no data', () => {
render(
<SharedDataTable
columns={columns}
data={[]}
totalCount={0}
pagination={{ pageIndex: 0, pageSize: 10 }}
onPaginationChange={vi.fn()}
/>
);
expect(screen.queryByText('No results.')).not.toBeNull();
});
});
@@ -21,6 +21,8 @@ import { type ReactNode, useEffect, useState } from 'react';
import { DataTablePagination } from './data-table-pagination';
const DEFAULT_RESET_FILTERS_DEPS: unknown[] = [];
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
@@ -58,7 +60,7 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
rowSelection,
onRowSelectionChange,
renderToolbar,
resetFiltersDeps = [],
resetFiltersDeps = DEFAULT_RESET_FILTERS_DEPS,
}: DataTableProps<TData, TValue>) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
@@ -88,13 +90,13 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
});
return (
<div className="flex flex-col gap-4 py-5 px-6 h-full overflow-auto relative">
<div className="relative flex h-full flex-col gap-4 overflow-auto px-6 py-5">
{renderToolbar?.(table)}
<div className="rounded-md border h-full flex flex-col overflow-auto relative">
<div className="relative flex h-full flex-col overflow-auto rounded-xl border border-border/60 bg-card shadow-1">
{loading ? (
<div className="absolute inset-0 z-10 bg-gray-50/70 backdrop-blur-[1px] flex flex-col items-center justify-center gap-2 text-sm text-gray-600">
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 bg-background/75 text-sm text-muted-foreground backdrop-blur-[1px]">
<svg
className="h-5 w-5 animate-spin text-gray-500"
className="h-5 w-5 animate-spin text-primary"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@@ -119,7 +121,10 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
<Table>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
<TableRow key={headerGroup.id} className="flex items-center">
<TableRow
key={headerGroup.id}
className="flex items-center bg-muted/40"
>
{headerGroup.headers.map(header => {
// Use meta.className if available, otherwise default to flex-1
const meta = header.column.columnDef.meta as
@@ -154,7 +159,7 @@ export function SharedDataTable<TData extends { id: string }, TValue>({
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
className="flex items-center"
className="flex items-center bg-card"
>
{row.getVisibleCells().map(cell => {
const meta = cell.column.columnDef.meta as
@@ -3,7 +3,6 @@ import { Label } from '@affine/admin/components/ui/label';
import { Separator } from '@affine/admin/components/ui/separator';
import { Switch } from '@affine/admin/components/ui/switch';
import type { FeatureType } from '@affine/graphql';
import { cssVarV2 } from '@toeverything/theme/v2';
import { useCallback } from 'react';
import { cn } from '../../utils';
@@ -42,10 +41,7 @@ export const FeatureToggleList = ({
if (!features.length) {
return (
<div
className={cn(className, 'px-3 py-2 text-xs')}
style={{ color: cssVarV2('text/secondary') }}
>
<div className={cn(className, 'px-3 py-2 text-xs text-muted-foreground')}>
No configurable features.
</div>
);
@@ -57,10 +53,10 @@ export const FeatureToggleList = ({
<div key={feature}>
<Label
className={cn(
'cursor-pointer',
'cursor-pointer transition-colors duration-100',
controlPosition === 'right'
? 'flex items-center justify-between p-3 text-[15px] gap-2 font-medium leading-6 overflow-hidden'
: 'flex items-center gap-2 px-3 py-2 text-sm'
? 'flex items-center justify-between p-3 text-sm gap-2 font-medium leading-6 overflow-hidden hover:bg-muted/40'
: 'flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted/40'
)}
>
{controlPosition === 'left' ? (
@@ -0,0 +1,35 @@
/**
* @vitest-environment happy-dom
*/
import { render, screen } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';
const { nextThemeProviderMock } = vi.hoisted(() => ({
nextThemeProviderMock: vi.fn(({ children }: { children?: any }) => (
<div data-testid="next-theme-provider">{children}</div>
)),
}));
vi.mock('next-themes', () => ({
ThemeProvider: nextThemeProviderMock,
}));
import { ThemeProvider } from './theme-provider';
describe('Admin ThemeProvider', () => {
test('uses the same dark/light/system behavior as main frontend', () => {
render(
<ThemeProvider>
<div>content</div>
</ThemeProvider>
);
expect(screen.queryByText('content')).not.toBeNull();
expect(nextThemeProviderMock).toHaveBeenCalledTimes(1);
const props = nextThemeProviderMock.mock.calls[0]?.[0] as any;
expect(props?.themes).toEqual(['dark', 'light']);
expect(props?.enableSystem).toBe(true);
expect(props?.defaultTheme).toBe('system');
});
});
@@ -0,0 +1,16 @@
import { ThemeProvider as NextThemeProvider } from 'next-themes';
import type { PropsWithChildren } from 'react';
const themes = ['dark', 'light'];
export const ThemeProvider = ({ children }: PropsWithChildren) => {
return (
<NextThemeProvider
themes={themes}
enableSystem={true}
defaultTheme="system"
>
{children}
</NextThemeProvider>
);
};
@@ -0,0 +1,36 @@
/**
* @vitest-environment happy-dom
*/
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, test } from 'vitest';
import { Button } from './button';
describe('Button', () => {
afterEach(() => {
cleanup();
});
test('uses token-aligned default styles', () => {
render(<Button>Save</Button>);
const button = screen.getByRole('button', { name: 'Save' });
expect(button.className).toContain('rounded-lg');
expect(button.className).toContain('bg-primary');
expect(button.className).toContain('focus-visible:ring-ring/30');
});
test('supports outline/sm variant with disabled state', () => {
render(
<Button variant="outline" size="sm" disabled={true}>
Cancel
</Button>
);
const button = screen.getByRole('button', { name: 'Cancel' });
expect(button.className).toContain('border-border');
expect(button.className).toContain('h-8');
expect(button.className).toContain('text-xs');
expect(button.hasAttribute('disabled')).toBe(true);
});
});
@@ -4,25 +4,25 @@ import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
'inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium transition-[background-color,color,border-color,box-shadow,transform] duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
default:
'bg-primary text-primary-foreground shadow-1 hover:bg-primary/90 active:translate-y-px',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
'bg-destructive text-destructive-foreground shadow-1 hover:bg-destructive/90 active:translate-y-px',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
'border border-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-muted',
ghost: 'text-muted-foreground hover:bg-accent hover:text-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
default: 'h-9 px-4',
sm: 'h-8 px-3 text-xs',
lg: 'h-10 px-6',
icon: 'h-9 w-9',
},
},
defaultVariants: {
@@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
'rounded-lg border bg-card text-card-foreground shadow-sm',
'rounded-xl border bg-card text-card-foreground shadow-sm transition-shadow duration-200',
className
)}
{...props}
@@ -4,7 +4,15 @@ import useEmblaCarousel, {
type UseEmblaCarouselType,
} from 'embla-carousel-react';
import { ArrowLeft, ArrowRight } from 'lucide-react';
import * as React from 'react';
import {
createContext,
forwardRef,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
@@ -27,10 +35,10 @@ type CarouselContextProps = {
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
const CarouselContext = createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
const context = useContext(CarouselContext);
if (!context) {
throw new Error('useCarousel must be used within a <Carousel />');
@@ -39,7 +47,7 @@ function useCarousel() {
return context;
}
const Carousel = React.forwardRef<
const Carousel = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
@@ -62,10 +70,10 @@ const Carousel = React.forwardRef<
},
plugins
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const [canScrollPrev, setCanScrollPrev] = useState(false);
const [canScrollNext, setCanScrollNext] = useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
const onSelect = useCallback((api: CarouselApi) => {
if (!api) {
return;
}
@@ -74,15 +82,15 @@ const Carousel = React.forwardRef<
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
const scrollPrev = useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
const scrollNext = useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault();
@@ -95,7 +103,7 @@ const Carousel = React.forwardRef<
[scrollPrev, scrollNext]
);
React.useEffect(() => {
useEffect(() => {
if (!api || !setApi) {
return;
}
@@ -103,7 +111,7 @@ const Carousel = React.forwardRef<
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
useEffect(() => {
if (!api) {
return;
}
@@ -117,20 +125,33 @@ const Carousel = React.forwardRef<
};
}, [api, onSelect]);
const resolvedOrientation =
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal');
const carouselContextValue = useMemo(
() => ({
carouselRef,
api,
opts,
orientation: resolvedOrientation,
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}),
[
api,
canScrollNext,
canScrollPrev,
carouselRef,
opts,
resolvedOrientation,
scrollNext,
scrollPrev,
]
);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<CarouselContext.Provider value={carouselContextValue}>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
@@ -147,7 +168,7 @@ const Carousel = React.forwardRef<
);
Carousel.displayName = 'Carousel';
const CarouselContent = React.forwardRef<
const CarouselContent = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
@@ -169,7 +190,7 @@ const CarouselContent = React.forwardRef<
});
CarouselContent.displayName = 'CarouselContent';
const CarouselItem = React.forwardRef<
const CarouselItem = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
@@ -191,7 +212,7 @@ const CarouselItem = React.forwardRef<
});
CarouselItem.displayName = 'CarouselItem';
const CarouselPrevious = React.forwardRef<
const CarouselPrevious = forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
@@ -220,7 +241,7 @@ const CarouselPrevious = React.forwardRef<
});
CarouselPrevious.displayName = 'CarouselPrevious';
const CarouselNext = React.forwardRef<
const CarouselNext = forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
@@ -1,5 +1,5 @@
import { cn } from '@affine/admin/utils';
import * as React from 'react';
import { createContext, forwardRef, useContext, useId, useMemo } from 'react';
import type { TooltipProps } from 'recharts';
import { ResponsiveContainer, Tooltip as RechartsTooltip } from 'recharts';
@@ -18,10 +18,10 @@ type ChartContextValue = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextValue | null>(null);
const ChartContext = createContext<ChartContextValue | null>(null);
function useChart() {
const value = React.useContext(ChartContext);
const value = useContext(ChartContext);
if (!value) {
throw new Error('useChart must be used within <ChartContainer />');
}
@@ -75,13 +75,14 @@ type ChartContainerProps = React.ComponentProps<'div'> & {
children: React.ComponentProps<typeof ResponsiveContainer>['children'];
};
const ChartContainer = React.forwardRef<HTMLDivElement, ChartContainerProps>(
const ChartContainer = forwardRef<HTMLDivElement, ChartContainerProps>(
({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const uniqueId = useId();
const chartId = `chart-${id ?? uniqueId.replace(/:/g, '')}`;
const chartContextValue = useMemo(() => ({ config }), [config]);
return (
<ChartContext.Provider value={{ config }}>
<ChartContext.Provider value={chartContextValue}>
<div
ref={ref}
data-chart={chartId}
@@ -113,61 +114,60 @@ type TooltipContentProps = {
valueFormatter?: (value: number, key: string) => React.ReactNode;
};
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
TooltipContentProps
>(({ active, payload, label, labelFormatter, valueFormatter }, ref) => {
const { config } = useChart();
const ChartTooltipContent = forwardRef<HTMLDivElement, TooltipContentProps>(
({ active, payload, label, labelFormatter, valueFormatter }, ref) => {
const { config } = useChart();
if (!active || !payload?.length) {
return null;
}
if (!active || !payload?.length) {
return null;
}
const title = labelFormatter ? labelFormatter(label ?? '', payload) : label;
const title = labelFormatter ? labelFormatter(label ?? '', payload) : label;
return (
<div
ref={ref}
className="min-w-44 rounded-md border bg-popover px-3 py-2 text-xs text-popover-foreground shadow-md"
>
{title ? (
<div className="mb-2 font-medium text-foreground/90">{title}</div>
) : null}
<div className="space-y-1">
{payload.map((item, index) => {
const dataKey = String(item.dataKey ?? item.name ?? index);
const itemConfig = config[dataKey];
const labelText = itemConfig?.label ?? item.name ?? dataKey;
const numericValue =
typeof item.value === 'number'
? item.value
: Number(item.value ?? 0);
const valueText = valueFormatter
? valueFormatter(numericValue, dataKey)
: numericValue;
const color = item.color ?? `var(--color-${dataKey})`;
return (
<div
ref={ref}
className="min-w-44 rounded-md border bg-popover px-3 py-2 text-xs text-popover-foreground shadow-md"
>
{title ? (
<div className="mb-2 font-medium text-foreground/90">{title}</div>
) : null}
<div className="space-y-1">
{payload.map((item, index) => {
const dataKey = String(item.dataKey ?? item.name ?? index);
const itemConfig = config[dataKey];
const labelText = itemConfig?.label ?? item.name ?? dataKey;
const numericValue =
typeof item.value === 'number'
? item.value
: Number(item.value ?? 0);
const valueText = valueFormatter
? valueFormatter(numericValue, dataKey)
: numericValue;
const color = item.color ?? `var(--color-${dataKey})`;
return (
<div
key={`${dataKey}-${index}`}
className="flex items-center gap-2"
>
<span
className="h-2 w-2 rounded-full"
style={{ backgroundColor: color }}
aria-hidden="true"
/>
<span className="text-muted-foreground">{labelText}</span>
<span className="ml-auto font-medium tabular-nums">
{valueText}
</span>
</div>
);
})}
return (
<div
key={`${dataKey}-${index}`}
className="flex items-center gap-2"
>
<span
className="h-2 w-2 rounded-full"
style={{ backgroundColor: color }}
aria-hidden="true"
/>
<span className="text-muted-foreground">{labelText}</span>
<span className="ml-auto font-medium tabular-nums">
{valueText}
</span>
</div>
);
})}
</div>
</div>
</div>
);
});
);
}
);
ChartTooltipContent.displayName = 'ChartTooltipContent';
export { ChartContainer, ChartTooltip, ChartTooltipContent };
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'fixed inset-0 z-50 bg-foreground/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
@@ -2,7 +2,7 @@ import { Label } from '@affine/admin/components/ui/label';
import { cn } from '@affine/admin/utils';
import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import * as React from 'react';
import { createContext, forwardRef, useContext, useId, useMemo } from 'react';
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
import { Controller, FormProvider, useFormContext } from 'react-hook-form';
@@ -15,7 +15,7 @@ type FormFieldContextValue<
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
const FormFieldContext = createContext<FormFieldContextValue>(
{} as FormFieldContextValue
);
@@ -25,16 +25,21 @@ const FormField = <
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
const formFieldContextValue = useMemo(
() => ({ name: props.name }),
[props.name]
);
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<FormFieldContext.Provider value={formFieldContextValue}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const fieldContext = useContext(FormFieldContext);
const itemContext = useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
@@ -59,26 +64,27 @@ type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
const FormItemContext = createContext<FormItemContextValue>(
{} as FormItemContextValue
);
const FormItem = React.forwardRef<
const FormItem = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
const id = useId();
const formItemContextValue = useMemo(() => ({ id }), [id]);
return (
<FormItemContext.Provider value={{ id }}>
<FormItemContext.Provider value={formItemContextValue}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = 'FormItem';
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
const FormLabel = forwardRef<
React.ComponentRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
@@ -94,8 +100,8 @@ const FormLabel = React.forwardRef<
});
FormLabel.displayName = 'FormLabel';
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
const FormControl = forwardRef<
React.ComponentRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
@@ -117,7 +123,7 @@ const FormControl = React.forwardRef<
});
FormControl.displayName = 'FormControl';
const FormDescription = React.forwardRef<
const FormDescription = forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
@@ -134,7 +140,7 @@ const FormDescription = React.forwardRef<
});
FormDescription.displayName = 'FormDescription';
const FormMessage = React.forwardRef<
const FormMessage = forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-9 w-full rounded-lg border border-input bg-background px-3 py-1.5 text-sm text-foreground ring-offset-background transition-[border-color,box-shadow,background-color] duration-150 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
@@ -16,7 +16,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
'flex h-9 w-full items-center justify-between rounded-lg border border-input bg-background px-3 py-1.5 text-sm ring-offset-background transition-[border-color,box-shadow,background-color] duration-150 placeholder:text-muted-foreground focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className
)}
{...props}
@@ -72,7 +72,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-[calc(var(--radius)-1px)] border border-border bg-popover text-popover-foreground shadow-menu data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
@@ -115,7 +115,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
@@ -18,7 +18,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'fixed inset-0 z-50 bg-foreground/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
@@ -6,7 +6,7 @@ function Skeleton({
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('animate-pulse rounded-md bg-muted', className)}
className={cn('animate-pulse rounded-lg bg-muted', className)}
{...props}
/>
);
@@ -13,7 +13,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
'group toast rounded-md group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-menu',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
@@ -8,7 +8,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-toggle-on data-[state=unchecked]:bg-toggle-off',
className
)}
{...props}
@@ -16,7 +16,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
'pointer-events-none block h-5 w-5 rounded-full bg-toggle-thumb shadow-sm ring-0 transition-transform duration-150 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
)}
/>
</SwitchPrimitives.Root>
@@ -57,7 +57,7 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
'border-b transition-colors duration-100 hover:bg-muted/50 data-[state=selected]:bg-muted',
className
)}
{...props}
@@ -8,7 +8,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex min-h-[96px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground ring-offset-background transition-[border-color,box-shadow,background-color] duration-150 placeholder:text-muted-foreground focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
@@ -2,39 +2,44 @@ import { toggleVariants } from '@affine/admin/components/ui/toggle';
import { cn } from '@affine/admin/utils';
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import type { VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { createContext, forwardRef, useContext, useMemo } from 'react';
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
const ToggleGroupContext = createContext<VariantProps<typeof toggleVariants>>({
size: 'default',
variant: 'default',
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
const ToggleGroup = forwardRef<
React.ComponentRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn('flex items-center justify-center gap-1', className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
>(({ className, variant, size, children, ...props }, ref) => {
const toggleGroupContextValue = useMemo(
() => ({ variant, size }),
[size, variant]
);
return (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn('flex items-center justify-center gap-1', className)}
{...props}
>
<ToggleGroupContext.Provider value={toggleGroupContextValue}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
});
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
const ToggleGroupItem = forwardRef<
React.ComponentRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
const context = useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
@@ -15,7 +15,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 overflow-hidden rounded-md border border-border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-menu animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}