mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-31 21:59:10 +08:00
feat: improve admin panel design (#14464)
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user