refactor(ui): adjust dataset page styles (#13452)

### What problem does this PR solve?

- Adjust UI styles in **Dataset** pages.
- Adjust several shared components styles
- Modify files and directory structure in `src/layouts`

### Type of change

- [x] Refactoring
This commit is contained in:
Jimmy Ben Klieve
2026-03-06 21:13:14 +08:00
committed by GitHub
parent 7166a7e50e
commit 094eae3cf5
44 changed files with 589 additions and 543 deletions

View File

@@ -1,8 +1,7 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { BrushCleaning } from 'lucide-react';
import { ReactNode, useCallback } from 'react';
import { ReactNode, useId } from 'react';
import { useTranslation } from 'react-i18next';
import {
ConfirmDeleteDialog,
@@ -30,58 +29,69 @@ export function BulkOperateBar({
className,
unit,
}: BulkOperateBarProps) {
const isDeleteItem = useCallback((id: string) => {
return id === 'delete';
}, []);
const { t } = useTranslation();
const ariaDescriptionId = useId();
return (
<Card className={cn('mb-4', className)}>
<CardContent className="p-1 pl-5 flex items-center gap-6">
<section className="text-text-sub-title-invert flex items-center gap-2">
<span>
{t('common.selected')}: {count}{' '}
{unit ?? t('knowledgeDetails.files')}
</span>
<BrushCleaning className="size-3" />
</section>
<Separator orientation={'vertical'} className="h-3"></Separator>
<Card
className={className}
role="menu"
aria-label={t('common.bulkOperate')}
aria-describedby={ariaDescriptionId}
>
<CardContent className="ps-5 pe-1 py-1 flex items-center gap-6">
<p
id={ariaDescriptionId}
className="text-sm text-text-secondary flex items-center gap-2"
>
{t('common.selected')}: {count} {unit ?? t('knowledgeDetails.files')}
<BrushCleaning className="size-[1em]" />
</p>
<Separator orientation={'vertical'} className="h-[1em]"></Separator>
<ul className="flex gap-2">
{list.map((x) => (
<li
key={x.id}
className={cn({ ['text-state-error']: isDeleteItem(x.id) })}
>
<ConfirmDeleteDialog
hidden={!isDeleteItem(x.id)}
onOk={x.onClick}
title={
unit
? t('common.delete') + ' ' + unit
: t('deleteModal.delFiles')
}
content={{
title: t('common.deleteThem'),
node: (
<ConfirmDeleteDialogNode
name={`${unit ? t('common.selected') + ' ' + count + ' ' + unit : t('deleteModal.delFilesContent', { count })}`}
></ConfirmDeleteDialogNode>
),
}}
{list.map((x) => {
const isDeleteItem = x.id === 'delete';
const buttonEl = (
<Button
variant={isDeleteItem ? 'danger' : 'outline'}
onClick={isDeleteItem ? () => {} : x.onClick}
role="menuitem"
>
<Button
variant={!isDeleteItem(x.id) ? 'ghost' : 'delete'}
onClick={isDeleteItem(x.id) ? () => {} : x.onClick}
className={cn({
['text-state-error border border-state-error bg-state-error/5']:
isDeleteItem(x.id),
})}
>
{x.icon} {x.label}
</Button>
</ConfirmDeleteDialog>
</li>
))}
{x.icon} {x.label}
</Button>
);
return (
<li key={x.id}>
{isDeleteItem ? (
<ConfirmDeleteDialog
key="deleteModal"
onOk={x.onClick}
title={
unit
? t('common.delete') + ' ' + unit
: t('deleteModal.delFiles')
}
content={{
title: t('common.deleteThem'),
node: (
<ConfirmDeleteDialogNode
name={`${unit ? t('common.selected') + ' ' + count + ' ' + unit : t('deleteModal.delFilesContent', { count })}`}
/>
),
}}
>
{buttonEl}
</ConfirmDeleteDialog>
) : (
buttonEl
)}
</li>
);
})}
</ul>
</CardContent>
</Card>

View File

@@ -10,7 +10,7 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Plus, X } from 'lucide-react';
import { LucidePlus, LucideTrash2 } from 'lucide-react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Separator } from '../ui/separator';
@@ -51,7 +51,9 @@ export function DynamicPageRange() {
</FormItem>
)}
/>
<Separator className="w-3 "></Separator>
<FormField
control={form.control}
name={`parser_config.pages.${index}.to`}
@@ -70,19 +72,27 @@ export function DynamicPageRange() {
</FormItem>
)}
/>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X />
<Button
className="ml-4"
size="icon"
variant="outline"
onClick={() => remove(index)}
>
<LucideTrash2 />
</Button>
</div>
);
})}
<Button
onClick={() => append({ from: 1, to: 100 })}
className="mt-4 border-dashed w-full"
variant={'outline'}
block
className="mt-4"
variant="dashed"
type="button"
>
<Plus />
<LucidePlus />
{t('knowledgeDetails.addPage')}
</Button>
</div>

View File

@@ -42,7 +42,6 @@ import { DataFlowSelect } from '../data-pipeline-select';
import { DelimiterFormField } from '../delimiter-form-field';
import { EntityTypesFormField } from '../entity-types-form-field';
import { ExcelToHtmlFormField } from '../excel-to-html-form-field';
import { FormContainer } from '../form-container';
import { LayoutRecognizeFormField } from '../layout-recognize-form-field';
import { MaxTokenNumberFormField } from '../max-token-number-from-field';
import { MinerUOptionsFormField } from '../mineru-options-form-field';
@@ -293,22 +292,16 @@ export function ChunkMethodDialog({
<DialogHeader>
<DialogTitle>{t('knowledgeDetails.chunkMethod')}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 max-h-[70vh] overflow-auto"
className="space-y-6 max-h-[70vh] overflow-auto -mx-6 px-10 py-5"
id={FormId}
>
<FormContainer>
<div className="space-y-6">
<ParseTypeItem />
{parseType === 1 && <ChunkMethodItem></ChunkMethodItem>}
{parseType === 2 && (
<DataFlowSelect
isMult={false}
// toDataPipeline={navigateToAgents}
formFieldName="pipeline_id"
/>
)}
{parseType === 1 && <ChunkMethodItem />}
{/* <FormField
control={form.control}
@@ -326,9 +319,9 @@ export function ChunkMethodDialog({
</FormItem>
)}
/> */}
{showPages && parseType === 1 && (
<DynamicPageRange></DynamicPageRange>
)}
{showPages && parseType === 1 && <DynamicPageRange />}
{showPages && parseType === 1 && layoutRecognize && (
<FormField
control={form.control}
@@ -341,31 +334,25 @@ export function ChunkMethodDialog({
{t('knowledgeDetails.taskPageSize')}
</FormLabel>
<FormControl>
<Input
{...field}
type={'number'}
min={1}
max={128}
></Input>
<Input {...field} type={'number'} min={1} max={128} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</FormContainer>
</div>
{parseType === 1 && (
<>
<FormContainer
show={showOne || showMaxTokenNumber}
className="space-y-3"
>
<div className="space-y-6 border-t-0.5 border-border-button pt-6 empty:hidden">
{showOne && (
<>
<LayoutRecognizeFormField showMineruOptions={false} />
{isMineruSelected && <MinerUOptionsFormField />}
</>
)}
{showMaxTokenNumber && (
<>
<MaxTokenNumberFormField
@@ -374,26 +361,21 @@ export function ChunkMethodDialog({
? 8192 * 2
: 2048
}
></MaxTokenNumberFormField>
<DelimiterFormField></DelimiterFormField>
/>
<DelimiterFormField />
<ChildrenDelimiterForm />
</>
)}
</FormContainer>
<FormContainer
show={
isMineruSelected ||
showAutoKeywords(selectedTag) ||
showExcelToHtml
}
className="space-y-3"
>
</div>
<div className="space-y-6 border-t-0.5 border-border-button pt-6 empty:hidden">
{selectedTag === DocumentParserType.Naive && (
<>
<EnableTocToggle />
<ImageContextWindow />
</>
)}
{showAutoKeywords(selectedTag) && (
<>
<AutoMetadata
@@ -404,28 +386,39 @@ export function ChunkMethodDialog({
<AutoQuestionsFormField></AutoQuestionsFormField>
</>
)}
{showExcelToHtml && (
<ExcelToHtmlFormField></ExcelToHtmlFormField>
)}
</FormContainer>
</div>
{/* {showRaptorParseConfiguration(
selectedTag as DocumentParserType,
) && (
<FormContainer>
<RaptorFormFields></RaptorFormFields>
</FormContainer>
)} */}
{/* {showGraphRagItems(selectedTag as DocumentParserType) &&
useGraphRag && (
selectedTag as DocumentParserType,
) && (
<FormContainer>
<UseGraphRagFormField></UseGraphRagFormField>
<RaptorFormFields></RaptorFormFields>
</FormContainer>
)} */}
{showEntityTypes && (
<EntityTypesFormField></EntityTypesFormField>
)}
{/* {showGraphRagItems(selectedTag as DocumentParserType) &&
useGraphRag && (
<FormContainer>
<UseGraphRagFormField></UseGraphRagFormField>
</FormContainer>
)} */}
<div className="space-y-6 border-t-0.5 border-border-button pt-6 empty:hidden">
{showEntityTypes && <EntityTypesFormField />}
</div>
</>
)}
<div className="space-y-6 empty:hidden">
{parseType === 2 && (
<DataFlowSelect
isMult={false}
// toDataPipeline={navigateToAgents}
formFieldName="pipeline_id"
/>
)}
</div>
</form>
</Form>
<DialogFooter>

View File

@@ -53,22 +53,24 @@ const Empty = (props: EmptyProps) => {
export default Empty;
export const EmptyCard = (props: EmptyCardProps) => {
const { icon, className, children, title, description, style } = props;
const { icon, className, children, title, description, style, ...restProps } =
props;
return (
<div
<article
className={cn(
'flex flex-col gap-3 items-start justify-start border border-dashed border-border-button rounded-md p-5 w-fit',
className,
)}
style={style}
{...restProps}
>
{icon}
{title && <div className="text-text-primary text-base">{title}</div>}
{title && <div className="text-text-primary text-sm">{title}</div>}
{description && (
<div className="text-text-secondary text-sm">{description}</div>
<p className="text-text-secondary text-sm">{description}</p>
)}
{children}
</div>
</article>
);
};
@@ -104,11 +106,14 @@ export const EmptyAppCard = (props: {
break;
}
return (
<div onClick={isSearch ? undefined : props.onClick} data-testid={testId}>
<div>
<EmptyCard
onClick={isSearch ? undefined : props.onClick}
data-testid={testId}
tabIndex={isSearch ? undefined : 0}
icon={showIcon ? cardData.icon : undefined}
title={isSearch ? notFound : title}
className={cn('cursor-pointer ', className)}
className={cn('cursor-pointer', className)}
style={style}
// description={EmptyCardData[type].description}
>

View File

@@ -15,4 +15,4 @@ export type EmptyCardProps = {
title?: string;
description?: string;
style?: React.CSSProperties;
};
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'title'>;

View File

@@ -1,5 +1,5 @@
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Card, CardContent } from '@/components/ui/card';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { formatDate } from '@/utils/date';
import { ReactNode } from 'react';
@@ -26,48 +26,61 @@ export function HomeCard({
}: IProps) {
return (
<Card
as="article"
data-testid={testId}
data-agent-name={data.name}
onClick={() => {
// navigateToSearch(data?.id);
onClick?.();
}}
tabIndex={0}
className="px-2.5 py-4 flex gap-2 items-start group h-full w-full hover:shadow-md"
>
<CardContent className="p-4 flex gap-2 items-start group h-full w-full hover:shadow-md">
<div className="flex justify-between mb-4">
<RAGFlowAvatar
className="w-[32px] h-[32px]"
avatar={data.avatar}
name={data.name}
/>
</div>
<div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]">
<section className="flex justify-between">
<section className="flex flex-1 min-w-0 gap-1 items-center">
<div
className="text-base font-bold leading-snug truncate"
data-testid="agent-name"
>
{data.name}
</div>
{icon}
</section>
{moreDropdown}
</section>
<div>
<RAGFlowAvatar
className="w-[32px] h-[32px]"
avatar={data.avatar}
name={data.name}
/>
</div>
<section className="flex flex-col gap-1 mt-1">
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
{data.description}
</div>
<div className="flex justify-between items-center">
<p className="text-sm opacity-80 whitespace-nowrap">
{formatDate(data.update_time)}
</p>
{sharedBadge}
</div>
</section>
</div>
</CardContent>
<div className="flex-1 w-0">
<CardHeader
as="header"
className="p-0 flex-1 flex flex-row items-center gap-2 space-y-0"
>
<CardTitle className="flex-1 inline-flex w-0 me-auto">
<h3
className="flex-1 truncate text-base font-bold leading-snug"
data-testid="agent-name"
>
{data.name}
</h3>
{icon}
</CardTitle>
<div>{moreDropdown}</div>
</CardHeader>
<CardContent className="p-0">
<div className="flex flex-col justify-between gap-1 flex-1 h-full w-[calc(100%-50px)]">
<section className="flex justify-between"></section>
<section className="flex flex-col gap-1 mt-1">
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
{data.description}
</div>
<div className="flex justify-between items-center">
<p className="text-sm opacity-80 whitespace-nowrap">
{formatDate(data.update_time)}
</p>
{sharedBadge}
</div>
</section>
</div>
</CardContent>
</div>
</Card>
);
}

View File

@@ -91,7 +91,7 @@ export default function ListFilterBar({
}, [value]);
return (
<div className={cn('flex justify-between mb-5 items-center', className)}>
<div className={cn('flex justify-between items-center', className)}>
<div className="text-2xl font-semibold flex items-center gap-2.5">
{typeof icon === 'string' ? (
// <IconFont name={icon} className="size-6"></IconFont>
@@ -101,7 +101,8 @@ export default function ListFilterBar({
)}
{leftPanel || title}
</div>
<div className="flex gap-5 items-center">
<div className="flex gap-4 items-center" role="toolbar">
{preChildren}
{showFilter && (
<FilterPopover
@@ -119,6 +120,7 @@ export default function ListFilterBar({
value={searchString}
onChange={onSearchChange}
className="w-32"
role="searchbox"
></SearchInput>
{children}
</div>

View File

@@ -9,10 +9,10 @@ export const MoreButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
<Button
ref={ref}
variant="ghost"
size={size || 'icon'}
size={size || 'icon-xs'}
className={cn(
'invisible size-3.5 bg-transparent group-hover:bg-transparent',
'group-focus-within:visible group-hover:visible aria-expanded:visible',
'opacity-0 size-3.5 transition-all bg-transparent group-hover:bg-transparent',
'group-focus-within:opacity-100 group-hover:opacity-100 aria-expanded:opacity-100',
className,
)}
{...props}

View File

@@ -1,6 +1,6 @@
import { cn } from '@/lib/utils';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { forwardRef, memo, useEffect, useRef, useState } from 'react';
import { forwardRef, memo, useMemo } from 'react';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
const PREDEFINED_COLORS = [
@@ -26,6 +26,15 @@ const getStringHash = (str: string): number => {
return Math.abs(hash);
};
const getInitials = (name?: string) => {
if (typeof name !== 'string' || !name) return '';
const parts = name?.trim().split(/\s+/);
if (parts.length === 1) {
return parts[0][0].toUpperCase();
}
return parts[0][0].toUpperCase();
};
const getColorForName = (name: string): { from: string; to: string } => {
const hash = getStringHash(name);
const index = hash % PREDEFINED_COLORS.length;
@@ -42,50 +51,15 @@ export const RAGFlowAvatar = memo(
}
>(({ name, avatar, isPerson = false, className, ...props }, ref) => {
// Generate initial letter logic
const getInitials = (name?: string) => {
if (typeof name !== 'string' || !name) return '';
const parts = name?.trim().split(/\s+/);
if (parts.length === 1) {
return parts[0][0].toUpperCase();
}
return parts[0][0].toUpperCase();
};
const initials = getInitials(name);
const { from, to } = name
? getColorForName(name)
: { from: 'hsl(0, 0%, 30%)', to: 'hsl(0, 0%, 80%)' };
const fallbackRef = useRef<HTMLElement>(null);
const [fontSize, setFontSize] = useState('0.875rem');
// Calculate font size
const calculateFontSize = () => {
if (fallbackRef.current) {
const containerWidth = fallbackRef.current.offsetWidth;
const newSize = containerWidth * 0.6;
setFontSize(`${newSize}px`);
}
};
useEffect(() => {
calculateFontSize();
if (fallbackRef.current) {
const resizeObserver = new ResizeObserver(() => {
calculateFontSize();
});
resizeObserver.observe(fallbackRef.current);
return () => {
if (fallbackRef.current) {
resizeObserver.unobserve(fallbackRef.current);
}
resizeObserver.disconnect();
};
}
}, []);
const { initials, from, to } = useMemo(
() => ({
initials: getInitials(name),
from: 'hsl(0, 0%, 30%)',
to: 'hsl(0, 0%, 80%)',
...(name ? getColorForName(name) : {}),
}),
[name],
);
return (
<Avatar
@@ -95,22 +69,27 @@ export const RAGFlowAvatar = memo(
>
<AvatarImage src={avatar} />
<AvatarFallback
ref={(node) => {
fallbackRef.current = node;
calculateFontSize();
}}
className={cn(
'bg-gradient-to-b',
'flex items-center justify-center',
'text-white ',
{ 'rounded-md': !isPerson },
)}
className="flex items-center justify-center bg-gradient-to-b text-white"
style={{
backgroundImage: `linear-gradient(to bottom, ${from}, ${to})`,
fontSize: fontSize,
}}
role="presentation"
aria-hidden="true"
>
{initials}
<svg
className="size-full block text-current select-none"
viewBox={`${-(50 + 22.5 * (initials.length - 1))} -50 ${100 + 45 * (initials.length - 1)} 100`}
preserveAspectRatio="xMinYMid meet"
>
<text
fontSize={55}
fill="currentColor"
textAnchor="middle"
dominantBaseline="central"
>
{initials}
</text>
</svg>
</AvatarFallback>
</Avatar>
);

View File

@@ -143,8 +143,8 @@ export const HomeIcon = ({
imgClass,
}: {
name: string;
height?: string;
width?: string;
height?: string | number;
width?: string | number;
imgClass?: string;
}) => {
const isDark = useIsDarkTheme();

View File

@@ -0,0 +1,56 @@
import { useIsDarkTheme, useTheme } from '@/components/theme-provider';
import { ThemeEnum } from '@/constants/common';
import { cn } from '@/lib/utils';
import { Root, Thumb } from '@radix-ui/react-switch';
import { LucideMoon, LucideSun } from 'lucide-react';
import { forwardRef } from 'react';
const ThemeSwitch = forwardRef<
React.ElementRef<typeof Root>,
React.ComponentPropsWithoutRef<typeof Root>
>(({ className, ...props }, ref) => {
const { setTheme } = useTheme();
const isDark = useIsDarkTheme();
return (
<Root
ref={ref}
className={cn(
'group/theme-switch relative rounded-full outline-none self-center focus-visible:ring-1 focus-visible:ring-accent-primary',
className,
)}
{...props}
checked={isDark}
onCheckedChange={(value) =>
setTheme(value ? ThemeEnum.Dark : ThemeEnum.Light)
}
>
<div className="self-center p-3 py-2 rounded-full bg-bg-card transition-[background-color]">
<div className="h-full flex items-center justify-between gap-4 relative z-[1] text-text-disabled transition-[text-color]">
<LucideSun
className={cn('size-[1em]', !isDark && 'text-text-primary')}
/>
<LucideMoon
className={cn('size-[1em]', isDark && 'text-text-primary')}
/>
</div>
</div>
<Thumb
className={cn(
'absolute top-0 left-0 w-[calc(50%+.25rem)] p-0.5 h-full rounded-full overflow-hidden',
'transition-all ease-out',
'group-hover/theme-switch:w-[calc(50%+.66rem)] group-focus-visible/theme-switch:w-[calc(50%+.66rem)]',
{
'left-[calc(50%-.25rem)] group-hover/theme-switch:left-[calc(50%-.66rem)] group-focus-visible/theme-switch:left-[calc(50%-.66rem)]':
isDark,
},
)}
>
<div className="size-full rounded-full bg-bg-base shadow-md transition-colors ease-out" />
</Thumb>
</Root>
);
});
export default ThemeSwitch;

View File

@@ -1,48 +0,0 @@
import { ThemeEnum } from '@/constants/common';
import { Moon, Sun } from 'lucide-react';
import { FC, useCallback } from 'react';
import { useIsDarkTheme, useTheme } from './theme-provider';
import { Button } from './ui/button';
const ThemeToggle: FC = () => {
const { setTheme } = useTheme();
const isDarkTheme = useIsDarkTheme();
const handleThemeChange = useCallback(
(checked: boolean) => {
setTheme(checked ? ThemeEnum.Dark : ThemeEnum.Light);
},
[setTheme],
);
return (
<Button
type="button"
onClick={() => handleThemeChange(!isDarkTheme)}
className="relative inline-flex h-6 w-14 items-center rounded-full transition-colors p-0.5 border-none focus:border-none bg-bg-card hover:bg-bg-card"
// aria-label={isDarkTheme ? 'Switch to light mode' : 'Switch to dark mode'}
>
<div className="inline-flex h-full w-full items-center">
<div
className={`inline-flex transform items-center justify-center rounded-full transition-transform ${
isDarkTheme
? ' text-text-disabled h-4 w-5'
: ' text-text-primary bg-bg-base h-full w-8 flex-1'
}`}
>
<Sun />
</div>
<div
className={`inline-flex transform items-center justify-center rounded-full transition-transform ${
isDarkTheme
? ' text-text-primary bg-bg-base h-full w-8 flex-1'
: 'text-text-disabled h-4 w-5'
}`}
>
<Moon />
</div>
</div>
</Button>
);
};
export default ThemeToggle;

View File

@@ -39,7 +39,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-bg-member',
'flex h-full w-full items-center justify-center bg-bg-member',
className,
)}
{...props}

View File

@@ -8,7 +8,7 @@ import { Link, LinkProps } from 'react-router';
const buttonVariants = cva(
cn(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors outline-0',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm transition-colors outline-0',
'disabled:pointer-events-none disabled:opacity-50 rounded border-0.5 border-transparent',
'[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0',
),
@@ -83,9 +83,13 @@ const buttonVariants = cva(
`,
link: 'text-primary underline-offset-4 hover:underline',
// Static
// Button has no interaction transitions
static: '',
},
size: {
auto: 'h-full px-1',
auto: '',
xl: 'h-12 rounded-xl px-5',
lg: 'h-10 rounded-lg px-4',
@@ -107,6 +111,8 @@ const buttonVariants = cva(
},
);
export type ButtonVariants = VariantProps<typeof buttonVariants>;
export type ButtonProps<IsAnchor extends boolean = false> = {
asChild?: boolean;
asLink?: boolean;
@@ -114,7 +120,7 @@ export type ButtonProps<IsAnchor extends boolean = false> = {
block?: boolean;
disabled?: boolean;
dot?: boolean;
} & VariantProps<typeof buttonVariants> &
} & ButtonVariants &
(IsAnchor extends true
? LinkProps
: React.ButtonHTMLAttributes<HTMLButtonElement>);
@@ -144,7 +150,7 @@ const Button = React.forwardRef(
<Comp
className={cn(
buttonVariants({ variant, size, className }),
{ 'block w-full': block },
{ 'w-full': block },
{ relative: dot },
)}
// @ts-ignore

View File

@@ -3,10 +3,10 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
HTMLElement,
React.HTMLAttributes<HTMLElement> & { as?: React.ElementType }
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn(
'rounded-lg border-border-button border-0.5 shadow-sm bg-bg-input transition-shadow',
@@ -19,9 +19,9 @@ Card.displayName = 'Card';
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
@@ -32,7 +32,7 @@ CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<
HTMLElement,
React.HTMLAttributes<HTMLElement> & { as?: React.ElementType }
>(({ className, as: As = 'div', ...props }, ref) => (
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn('text-2xl leading-normal font-medium', className)}
@@ -43,9 +43,9 @@ CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn('text-sm text-text-secondary', className)}
{...props}
@@ -55,9 +55,9 @@ CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn('p-6 pt-0 transition-shadow', className)}
{...props}
@@ -67,9 +67,9 @@ CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
React.HTMLAttributes<HTMLDivElement> & { as?: React.ElementType }
>(({ as: As = 'div', className, ...props }, ref) => (
<As
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}

View File

@@ -38,7 +38,10 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'outline-0 fixed left-[50%] top-[50%] rounded-lg z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4',
'outline-none outline-0 fixed left-[50%] top-[50%] rounded-lg z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%]',
// TODO: to keep scrollbar perfectly aligned to header bottom and/or footer top,
// 'gap-4' should be removed, then bring your own body container with padding-y instead.
'gap-4',
'border-0.5 border-border-button bg-bg-base p-6 shadow-lg duration-200 sm:rounded-lg',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
@@ -88,7 +91,7 @@ const DialogFooter = ({
<div
className={cn(
// '-mx-6 -mb-6 px-12 pt-4 pb-8',
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4',
'-mx-6 -mb-6 p-6 flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4',
className,
)}
{...props}

View File

@@ -18,7 +18,13 @@ const HoverCardContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-fit max-w-96 overflow-auto break-words whitespace-pre-wrap rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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 origin-[--radix-hover-card-content-transform-origin]',
'z-50 w-fit max-w-96 overflow-auto break-words whitespace-pre-wrap',
'rounded-md border bg-bg-base p-4 text-text-primary shadow-md outline-none',
'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',
'origin-[--radix-hover-card-content-transform-origin]',
className,
)}
{...props}

View File

@@ -2,6 +2,7 @@ import { cn } from '@/lib/utils';
import React, { useContext, useState } from 'react';
const RadioGroupContext = React.createContext<{
name?: string;
value: string | number;
onChange: (value: string | number) => void;
disabled?: boolean;
@@ -50,30 +51,46 @@ function Radio({
return (
<label
className={cn(
'flex items-center cursor-pointer gap-2 text-sm',
'group/radio relative flex items-center cursor-pointer gap-2 text-sm',
mergedDisabled && 'cursor-not-allowed opacity-50',
)}
>
<span
<input
type="radio"
name={groupContext?.name}
value={value}
checked={isChecked}
onClick={handleClick}
disabled={mergedDisabled}
data-testid={testId}
className="peer absolute size-[1px] opacity-0"
/>
<div
className={cn(
'flex h-4 w-4 items-center justify-center rounded-full border border-border transition-colors',
'peer outline-none focus-visible:border-border-button',
'flex h-4 w-4 items-center justify-center rounded-full border border-border-button transition-colors',
'group-hover/radio:border-border-default hover:border-border-default',
'peer-focus:border-primary',
isChecked && 'border-primary bg-primary/10',
mergedDisabled && 'border-muted',
)}
onClick={handleClick}
data-testid={testId}
>
{isChecked && (
<div className="h-3 w-3 fill-primary text-primary bg-text-primary rounded-full" />
)}
</span>
<div
className={cn(
'h-2 w-2 fill-primary text-primary bg-text-primary rounded-full opacity-0 scale-0 transition-all',
isChecked && 'opacity-100 scale-100',
)}
/>
</div>
{children && <span className="text-foreground">{children}</span>}
</label>
);
}
type RadioGroupProps = {
name?: string;
value?: string | number;
defaultValue?: string | number;
onChange?: (value: string | number) => void;
@@ -86,6 +103,7 @@ type RadioGroupProps = {
const Group = React.forwardRef<HTMLDivElement, RadioGroupProps>(
(
{
name,
value,
defaultValue,
onChange,
@@ -116,6 +134,7 @@ const Group = React.forwardRef<HTMLDivElement, RadioGroupProps>(
return (
<RadioGroupContext.Provider
value={{
name,
value: mergedValue,
onChange: handleChange,
disabled,

View File

@@ -1,5 +1,6 @@
import { cn } from '@/lib/utils';
import * as React from 'react';
import { Button, ButtonVariants } from './button';
export declare type SegmentedValue = string | number;
export declare type SegmentedRawOption = SegmentedValue;
export interface SegmentedLabeledOption {
@@ -56,7 +57,7 @@ export interface SegmentedProps extends Omit<
itemClassName?: string;
rounded?: keyof typeof segmentedVariants.round;
sizeType?: keyof typeof segmentedVariants.size;
buttonSize?: keyof typeof segmentedVariants.buttonSize;
buttonSize?: ButtonVariants['size'];
}
export const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
@@ -101,12 +102,12 @@ export const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
const actualValue = isObject ? option.value : option;
return (
<div
<Button
key={actualValue}
type="button"
size={buttonSize}
variant="static"
className={cn(
'inline-flex items-center text-base font-normal cursor-pointer',
segmentedVariants.round[rounded],
segmentedVariants.buttonSize[buttonSize],
{
'text-text-primary bg-bg-base': selectedValue === actualValue,
},
@@ -118,7 +119,7 @@ export const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
onClick={() => handleOnChange(actualValue)}
>
{isObject ? option.label : option}
</div>
</Button>
);
})}
</div>