revert: undo t0ng7u UI design-system refactor

Revert the following commits:
- 308e3e347 feat(web): polish themed data views and add task log details
- b2a890e75 fix: Fontsource asset resolution across workspace layouts
- 9d1ca545e refactor(web): refine data-table cards and pricing page layout
- 0918bdb49 refactor(web): consolidate design-system primitives and responsive data views
- 262ab9312 style(web): unify design system across default frontend
This commit is contained in:
CaIon
2026-07-11 14:51:54 +08:00
parent ad900bbba7
commit 337169e0a4
512 changed files with 10188 additions and 10268 deletions
+3 -3
View File
@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
import type { ComponentProps } from 'react'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
@@ -48,13 +48,13 @@ export const Action = ({
label,
className,
variant = 'ghost',
size = 'icon',
size = 'sm',
...props
}: ActionProps) => {
const button = (
<Button
className={cn(
'text-muted-foreground hover:text-foreground relative',
'text-muted-foreground hover:text-foreground relative size-9 p-1.5',
className
)}
size={size}
+11 -5
View File
@@ -22,7 +22,7 @@ import { type LucideIcon, XIcon } from 'lucide-react'
import type { ComponentProps, HTMLAttributes } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
@@ -63,14 +63,17 @@ export type ArtifactCloseProps = ComponentProps<typeof Button>
export const ArtifactClose = ({
className,
children,
size = 'icon',
size = 'sm',
variant = 'ghost',
...props
}: ArtifactCloseProps) => {
const { t } = useTranslation()
return (
<Button
className={cn('text-muted-foreground hover:text-foreground', className)}
className={cn(
'text-muted-foreground hover:text-foreground size-8 p-0',
className
)}
size={size}
type='button'
variant={variant}
@@ -121,13 +124,16 @@ export const ArtifactAction = ({
icon: Icon,
children,
className,
size = 'icon',
size = 'sm',
variant = 'ghost',
...props
}: ArtifactActionProps) => {
const button = (
<Button
className={cn('text-muted-foreground hover:text-foreground', className)}
className={cn(
'text-muted-foreground hover:text-foreground size-8 p-0',
className
)}
size={size}
type='button'
variant={variant}
+5 -5
View File
@@ -32,7 +32,7 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
type BranchContextType = {
@@ -178,14 +178,14 @@ export const BranchPrevious = ({
<Button
aria-label={t('Previous branch')}
className={cn(
'text-muted-foreground shrink-0 transition-colors',
'text-muted-foreground size-7 shrink-0 transition-colors',
'hover:bg-accent hover:text-foreground',
'disabled:pointer-events-none disabled:opacity-50',
className
)}
disabled={totalBranches <= 1}
onClick={goToPrevious}
size='icon-sm'
size='icon'
type='button'
variant='ghost'
{...props}
@@ -209,14 +209,14 @@ export const BranchNext = ({
<Button
aria-label={t('Next branch')}
className={cn(
'text-muted-foreground shrink-0 transition-colors',
'text-muted-foreground size-7 shrink-0 transition-colors',
'hover:bg-accent hover:text-foreground',
'disabled:pointer-events-none disabled:opacity-50',
className
)}
disabled={totalBranches <= 1}
onClick={goToNext}
size='icon-sm'
size='icon'
type='button'
variant='ghost'
{...props}
+8 -6
View File
@@ -46,7 +46,7 @@ import {
import { useTranslation } from 'react-i18next'
import type { BundledLanguage } from 'shiki'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
@@ -413,7 +413,7 @@ export const CodeBlockFrame = ({
{showToolbar && (
<div className='bg-muted/35 border-border/70 flex min-h-10 items-center gap-2 border-b px-2 py-1.5'>
<div className='min-w-0 flex-1'>
<div className='text-muted-foreground truncate font-mono text-xs font-medium tracking-wide uppercase'>
<div className='text-muted-foreground truncate font-mono text-[11px] font-medium tracking-wide uppercase'>
{title}
</div>
</div>
@@ -507,8 +507,9 @@ export const CodeBlock = ({
render={
<Button
aria-label={isCodeCollapsed ? t('Expand') : t('Collapse')}
className='size-8'
onClick={() => setIsCollapsed((value) => !value)}
size='icon'
size='icon-sm'
type='button'
variant='ghost'
>
@@ -531,8 +532,9 @@ export const CodeBlock = ({
render={
<Button
aria-label={t('Download')}
className='size-8'
onClick={downloadCode}
size='icon'
size='icon-sm'
type='button'
variant='ghost'
>
@@ -639,9 +641,9 @@ export const CodeBlockCopyButton = ({
const button = (
<Button
aria-label={isCopied ? t('Copied!') : t('Copy code')}
className={cn('shrink-0', className)}
className={cn('size-8 shrink-0', className)}
onClick={copyToClipboard}
size='icon'
size='icon-sm'
type='button'
variant='ghost'
{...props}
+2 -2
View File
@@ -26,8 +26,8 @@ import {
useContext,
} from 'react'
import { Button } from '@/components/design-system/button'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
// Workaround for missing types in 'ai' package
@@ -169,5 +169,5 @@ export const ConfirmationActions = ({
export type ConfirmationActionProps = ComponentProps<typeof Button>
export const ConfirmationAction = (props: ConfirmationActionProps) => (
<Button type='button' {...props} />
<Button className='h-8 px-3 text-sm' type='button' {...props} />
)
+1 -1
View File
@@ -23,7 +23,7 @@ import { type ComponentProps, createContext, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { getUsage } from 'tokenlens'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
HoverCard,
HoverCardContent,
+1 -1
View File
@@ -23,7 +23,7 @@ import { type ComponentProps, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
export type ConversationProps = ComponentProps<typeof StickToBottom>
+1 -1
View File
@@ -29,8 +29,8 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Carousel,
type CarouselApi,
+1 -1
View File
@@ -24,7 +24,7 @@ import {
import { type ComponentProps, createContext, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
+2 -2
View File
@@ -22,7 +22,7 @@ import { ChevronsUpDownIcon } from 'lucide-react'
import { type ComponentProps, createContext, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Card,
CardAction,
@@ -156,7 +156,7 @@ export const PlanTrigger = ({ className, ...props }: PlanTriggerProps) => {
<CollapsibleTrigger
render={
<Button
className={cn(className)}
className={cn('size-8', className)}
data-slot='plan-trigger'
size='icon'
variant='ghost'
+15 -16
View File
@@ -55,7 +55,7 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
@@ -64,20 +64,7 @@ import {
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/design-system/command'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from '@/components/design-system/input-group'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/design-system/select'
} from '@/components/ui/command'
import {
DropdownMenu,
DropdownMenuContent,
@@ -89,6 +76,19 @@ import {
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from '@/components/ui/input-group'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { cn } from '@/lib/utils'
// ============================================================================
@@ -326,7 +326,6 @@ export function PromptInputAttachment({
</div>
<Button
aria-label={t('Remove attachment')}
size='icon-xs'
className='absolute inset-0 size-5 cursor-pointer rounded p-0 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 [&>svg]:size-2.5'
onClick={(e) => {
e.stopPropagation()
+2 -2
View File
@@ -21,7 +21,7 @@ For commercial licensing, please contact support@quantumnous.com
import { ChevronDownIcon, PaperclipIcon } from 'lucide-react'
import type { ComponentProps } from 'react'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Collapsible,
CollapsibleContent,
@@ -236,7 +236,7 @@ export const QueueSectionTrigger = ({
<Button
variant='ghost'
className={cn(
'group bg-muted/40 text-muted-foreground hover:bg-muted h-auto sm:h-auto w-full justify-between px-3 py-2 text-left',
'group bg-muted/40 text-muted-foreground hover:bg-muted h-auto w-full justify-between px-3 py-2 text-left',
className
)}
type='button'
@@ -32,28 +32,32 @@ import type {
const alertConfig = {
note: {
label: 'Note',
className: 'border-info/25 bg-info/8 text-foreground',
markerClassName: 'text-status-info',
className:
'border-blue-500/40 bg-blue-500/8 text-blue-950 dark:text-blue-100',
markerClassName: 'text-blue-600 dark:text-blue-300',
},
tip: {
label: 'Tip',
className: 'border-success/25 bg-success/8 text-foreground',
markerClassName: 'text-status-success',
className:
'border-emerald-500/40 bg-emerald-500/8 text-emerald-950 dark:text-emerald-100',
markerClassName: 'text-emerald-600 dark:text-emerald-300',
},
important: {
label: 'Important',
className: 'border-border bg-muted/40 text-foreground',
markerClassName: 'text-foreground',
className:
'border-violet-500/40 bg-violet-500/8 text-violet-950 dark:text-violet-100',
markerClassName: 'text-violet-600 dark:text-violet-300',
},
warning: {
label: 'Warning',
className: 'border-warning/30 bg-warning/8 text-foreground',
markerClassName: 'text-status-warning',
className:
'border-amber-500/40 bg-amber-500/8 text-amber-950 dark:text-amber-100',
markerClassName: 'text-amber-600 dark:text-amber-300',
},
caution: {
label: 'Caution',
className: 'border-destructive/25 bg-destructive/8 text-foreground',
markerClassName: 'text-status-destructive',
className: 'border-red-500/40 bg-red-500/8 text-red-950 dark:text-red-100',
markerClassName: 'text-red-600 dark:text-red-300',
},
} satisfies Record<AlertKind, AlertConfig>
+1 -1
View File
@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
import type { ComponentProps } from 'react'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { cn } from '@/lib/utils'
+5 -5
View File
@@ -29,13 +29,13 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Input } from '@/components/design-system/input'
import { Button } from '@/components/ui/button'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import { Input } from '@/components/ui/input'
import {
Tooltip,
TooltipContent,
@@ -135,10 +135,10 @@ export const WebPreviewNavigationButton = ({
<TooltipTrigger
render={
<Button
className='hover:text-foreground'
className='hover:text-foreground h-8 w-8 p-0'
disabled={disabled}
onClick={onClick}
size='icon'
size='sm'
variant='ghost'
{...props}
/>
@@ -185,7 +185,7 @@ export const WebPreviewUrl = ({
return (
<Input
className='flex-1 text-sm'
className='h-8 flex-1 text-sm'
onChange={onChange ?? handleChange}
onKeyDown={handleKeyDown}
placeholder={t('Enter URL...')}
+1 -1
View File
@@ -30,7 +30,7 @@ import {
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/design-system/command'
} from '@/components/ui/command'
import { useSearch } from '@/context/search-provider'
import { useTheme } from '@/context/theme-provider'
import { useSidebarData } from '@/hooks/use-sidebar-data'
+5 -3
View File
@@ -32,14 +32,13 @@ import { IconSidebarSidebar } from '@/assets/custom/icon-sidebar-sidebar'
import { IconThemeDark } from '@/assets/custom/icon-theme-dark'
import { IconThemeLight } from '@/assets/custom/icon-theme-light'
import { IconThemeSystem } from '@/assets/custom/icon-theme-system'
import { Button } from '@/components/design-system/button'
import { useSidebar } from '@/components/design-system/sidebar'
import {
sideDrawerContentClassName,
sideDrawerFooterClassName,
sideDrawerFormClassName,
sideDrawerHeaderClassName,
} from '@/components/drawer-layout'
import { Button } from '@/components/ui/button'
import {
Sheet,
SheetContent,
@@ -63,6 +62,8 @@ import {
} from '@/lib/theme-customization'
import { cn } from '@/lib/utils'
import { useSidebar } from './ui/sidebar'
const Item = RadioPrimitive.Root
export function ConfigDrawer() {
@@ -144,8 +145,9 @@ function SectionTitle(props: {
{props.title}
{props.showReset && props.onReset && (
<Button
size='icon-xs'
size='icon'
variant='secondary'
className='size-4'
onClick={props.onReset}
aria-label='Reset'
>
+2 -2
View File
@@ -26,8 +26,8 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/design-system/alert-dialog'
import { Button } from '@/components/design-system/button'
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
type ConfirmDialogProps = {
+2 -2
View File
@@ -20,7 +20,7 @@ import { Check, Copy } from 'lucide-react'
import { type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
@@ -35,7 +35,7 @@ interface CopyButtonProps {
className?: string
iconClassName?: string
variant?: 'ghost' | 'outline' | 'default' | 'secondary' | 'destructive'
size?: 'default' | 'sm' | 'lg' | 'icon' | 'icon-sm' | 'icon-xs'
size?: 'default' | 'sm' | 'lg' | 'icon'
tooltip?: string
successTooltip?: string
'aria-label'?: string
+1 -1
View File
@@ -27,7 +27,7 @@ export function BadgeCell({ className, ...props }: BadgeCellProps) {
<div
data-slot='badge-cell'
className={cn(
'flex max-w-full min-w-0 items-center gap-1 overflow-visible',
'flex max-w-full min-w-0 items-center gap-1 overflow-hidden [&_[data-slot=status-badge]]:max-w-full [&_[data-slot=status-badge]]:min-w-0',
className
)}
{...props}
@@ -1,24 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { createContext } from 'react'
export type BadgeListCellDisplay = 'compact' | 'full'
export const BadgeListCellDisplayContext =
createContext<BadgeListCellDisplay>('compact')
@@ -26,8 +26,6 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip'
import { BadgeListCellDisplayContext } from './badge-list-cell-context'
interface BadgeListCellProps {
items: React.ReactNode[]
max?: number
@@ -35,31 +33,18 @@ interface BadgeListCellProps {
}
/**
* Badge collection that stays compact in table cells and can expose every
* item when rendered inside a detail-oriented card.
* Table cell renderer for a list of badges with overflow tooltip.
* Displays up to `max` badges inline; remaining items appear in a tooltip.
*/
export function BadgeListCell({
items,
max = 2,
tooltipClassName,
}: BadgeListCellProps) {
const display = React.useContext(BadgeListCellDisplayContext)
if (items.length === 0) {
return <span className='text-muted-foreground text-xs'>-</span>
}
if (display === 'full') {
return (
<StatusBadgeList
items={items}
max={items.length}
renderItem={(item) => item}
className='flex-wrap overflow-visible'
/>
)
}
const showTooltip = items.length > max
return (
@@ -25,7 +25,7 @@ import {
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
@@ -58,7 +58,8 @@ export function DataTableColumnHeader<TData, TValue>({
render={
<Button
variant='ghost'
className='data-popup-open:bg-accent -ms-3'
size='sm'
className='data-popup-open:bg-accent -ms-3 h-8'
/>
}
>
@@ -60,8 +60,8 @@ function getPinnedColumnClassName(
) {
const edgeClassName =
pinnedColumn.side === 'left'
? 'shadow-[8px_0_10px_-10px_var(--foreground)]'
: 'shadow-[-8px_0_10px_-10px_var(--foreground)]'
? 'shadow-[8px_0_10px_-10px_hsl(var(--foreground))]'
: 'shadow-[-8px_0_10px_-10px_hsl(var(--foreground))]'
return cn(
'sticky whitespace-nowrap',
@@ -16,9 +16,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
export function isContentSizedColumn(
columnId: string,
contentSized = false
): boolean {
return contentSized || columnId === 'actions'
export function isContentSizedColumn(columnId: string): boolean {
return columnId === 'actions'
}
@@ -27,8 +27,7 @@ export function DataTableColgroup<TData>({
}) {
const columns = table.getVisibleLeafColumns()
const sizedColumns = columns.filter(
(column) =>
!isContentSizedColumn(column.id, column.columnDef.meta?.contentSized)
(column) => !isContentSizedColumn(column.id)
)
const totalSize = sizedColumns.reduce((sum, col) => sum + col.getSize(), 0)
@@ -39,8 +38,7 @@ export function DataTableColgroup<TData>({
table,
column.id,
column.getSize(),
totalSize,
column.columnDef.meta?.contentSized
totalSize
)
return <col key={column.id} style={{ width }} />
@@ -53,10 +51,9 @@ function getColumnWidth<TData>(
table: TanstackTable<TData>,
columnId: string,
columnSize: number,
totalSize: number,
contentSized?: boolean
totalSize: number
) {
if (isContentSizedColumn(columnId, contentSized)) {
if (isContentSizedColumn(columnId)) {
return undefined
}
@@ -24,11 +24,7 @@ import {
import type { KeyboardEvent, MouseEvent } from 'react'
import { useTranslation } from 'react-i18next'
import {
TableHead,
TableHeader,
TableRow,
} from '@/components/design-system/table'
import { TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { cn } from '@/lib/utils'
import { DataTableColumnHeader } from './column-header'
@@ -249,10 +245,7 @@ function shouldRenderColumnResizer<TData>(
table.options.enableColumnResizing === true &&
!header.isPlaceholder &&
header.column.getCanResize() &&
!isContentSizedColumn(
header.column.id,
header.column.columnDef.meta?.contentSized
)
!isContentSizedColumn(header.column.id)
)
}
@@ -260,13 +253,7 @@ function getHeaderSizeStyle<TData>(
header: Header<TData, unknown>,
applyHeaderSize: boolean | undefined
) {
if (
!applyHeaderSize ||
isContentSizedColumn(
header.column.id,
header.column.columnDef.meta?.contentSized
)
) {
if (!applyHeaderSize || isContentSizedColumn(header.column.id)) {
return undefined
}
+62 -12
View File
@@ -16,18 +16,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { flexRender, type Row } from '@tanstack/react-table'
import {
flexRender,
type Cell,
type Row,
type Table as TanstackTable,
} from '@tanstack/react-table'
import * as React from 'react'
import { TableCell, TableRow } from '@/components/design-system/table'
import { TableCell, TableRow } from '@/components/ui/table'
import { cn } from '@/lib/utils'
import { TruncatedCell } from './truncated-cell'
import type { DataTableColumnClassName } from './types'
type DataTableRowProps<TData> = {
row: Row<TData>
className?: string
getColumnClassName?: DataTableColumnClassName
cellRenderColumns?: TanstackTable<TData>['options']['columns']
} & Omit<React.ComponentProps<typeof TableRow>, 'children'>
type DataTableRowInnerProps<TData> = DataTableRowProps<TData> & {
@@ -39,8 +46,13 @@ function DataTableRowInner<TData>({
isSelected,
className,
getColumnClassName,
cellRenderColumns,
...rowProps
}: DataTableRowInnerProps<TData>) {
// Destructured only to keep it out of `rowProps` (it is not a valid DOM attr)
// and to feed the memo comparator below; it is intentionally unused here.
void cellRenderColumns
return (
<TableRow
data-state={isSelected ? 'selected' : undefined}
@@ -48,25 +60,19 @@ function DataTableRowInner<TData>({
{...rowProps}
>
{row.getVisibleCells().map((cell) => {
const contentMode = cell.column.columnDef.meta?.contentMode ?? 'wrap'
const renderedCell = renderCellContent(cell)
return (
<TableCell
key={cell.id}
data-column-id={cell.column.id}
data-content-mode={contentMode}
className={cn(
'max-w-full min-w-0',
contentMode === 'full' &&
'max-w-none overflow-visible [&_.truncate]:overflow-visible [&_.truncate]:text-clip',
contentMode === 'wrap' &&
'whitespace-normal break-words [overflow-wrap:anywhere] [&_.truncate]:overflow-visible [&_.truncate]:text-clip [&_.truncate]:whitespace-normal',
contentMode === 'summary' &&
'whitespace-normal break-words [overflow-wrap:anywhere]',
renderedCell.isPrimitive && 'overflow-hidden',
getColumnClassName?.(cell.column.id, 'cell')
)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{renderedCell.content}
</TableCell>
)
})}
@@ -74,6 +80,50 @@ function DataTableRowInner<TData>({
)
}
const MemoizedDataTableRow = React.memo(DataTableRowInner, (prev, next) => {
// Do not read row.getIsSelected() inside the comparator: TanStack row objects
// keep a stable reference while their selection state mutates, so reading it
// here compares identical live values and misses selection changes. Selection
// is lifted to the `isSelected` prop, captured per render in DataTableRow.
//
// Column cell renderers (and getColumnClassName) can close over external
// state while the row stays stable, so column definitions and the class
// resolver are part of the render identity and must be compared too.
return (
prev.row === next.row &&
prev.className === next.className &&
prev.isSelected === next.isSelected &&
prev.getColumnClassName === next.getColumnClassName &&
prev.cellRenderColumns === next.cellRenderColumns
)
}) as typeof DataTableRowInner
export function DataTableRow<TData>(props: DataTableRowProps<TData>) {
return <DataTableRowInner {...props} isSelected={props.row.getIsSelected()} />
return (
<MemoizedDataTableRow {...props} isSelected={props.row.getIsSelected()} />
)
}
function renderCellContent<TData>(cell: Cell<TData, unknown>) {
const content = flexRender(cell.column.columnDef.cell, cell.getContext())
const textContent = getPrimitiveTextContent(content)
if (!textContent) {
return { content, isPrimitive: false }
}
return {
content: (
<TruncatedCell tooltipContent={textContent}>{content}</TruncatedCell>
),
isPrimitive: true,
}
}
function getPrimitiveTextContent(content: React.ReactNode): string | null {
if (typeof content === 'string' || typeof content === 'number') {
return String(content)
}
return null
}
@@ -19,12 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import type { Row, Table as TanstackTable } from '@tanstack/react-table'
import * as React from 'react'
import {
Table,
TableBody,
TableCell,
TableRow,
} from '@/components/design-system/table'
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'
import { cn } from '@/lib/utils'
import {
@@ -54,7 +49,10 @@ export { DataTableRowActionMenu } from './row-action-menu'
export function DataTableView<TData>(props: DataTableViewProps<TData>) {
const rows = props.rows ?? props.table.getRowModel().rows
const colSpan = props.table.getVisibleLeafColumns().length
const colSpan = React.useMemo(
() => props.table.getVisibleLeafColumns().length,
[props.table]
)
const columnClassName = useResolvedColumnClassName(
props.table,
props.getColumnClassName,
@@ -64,7 +62,7 @@ export function DataTableView<TData>(props: DataTableViewProps<TData>) {
return (
<div
className={cn(
'w-full min-w-0 overflow-hidden rounded-lg border',
'overflow-hidden rounded-lg border',
props.containerClassName
)}
{...props.containerProps}
@@ -103,15 +101,7 @@ function UnifiedTableView<TData>({
return (
<div className={props.tableContainerClassName}>
<Table
className={props.tableClassName}
style={tableSizing.style}
containerProps={{
role: props.scrollLabel ? 'region' : undefined,
tabIndex: 0,
'aria-label': props.scrollLabel,
}}
>
<Table className={props.tableClassName} style={tableSizing.style}>
{tableSizing.colgroup}
<DataTableHeader
table={props.table}
@@ -148,15 +138,12 @@ function SplitHeaderTableView<TData>({
>
<div
className={cn(
'min-h-0 min-w-0 flex-1 overscroll-contain overflow-auto',
'min-h-0 flex-1 overflow-auto',
'**:data-[slot=table-header]:[--table-header-bg:var(--table-header)]',
'**:data-[slot=table-header]:bg-(--table-header-bg)',
props.splitHeaderScrollClassName,
props.bodyContainerClassName
)}
role={props.scrollLabel ? 'region' : undefined}
tabIndex={0}
aria-label={props.scrollLabel}
>
<table
data-slot='table'
@@ -170,7 +157,7 @@ function SplitHeaderTableView<TData>({
<DataTableHeader
table={props.table}
applyHeaderSize={props.applyHeaderSize}
className={cn('sticky top-0 z-20', props.tableHeaderClassName)}
className={cn('sticky top-0 z-10', props.tableHeaderClassName)}
rowClassName={props.tableHeaderRowClassName}
getColumnClassName={getColumnClassName}
/>
@@ -338,6 +325,7 @@ function renderDefaultRow<TData>(
row={row}
className={cn(props.tableBodyRowClassName, props.getRowClassName?.(row))}
getColumnClassName={getColumnClassName}
cellRenderColumns={props.table.options.columns}
/>
)
}
+8 -12
View File
@@ -25,7 +25,7 @@ import {
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
@@ -33,7 +33,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/design-system/select'
} from '@/components/ui/select'
import { cn, getPageNumbers } from '@/lib/utils'
type DataTablePaginationProps<TData> = {
@@ -83,7 +83,7 @@ export function DataTablePagination<TData>({
table.setPageSize(Number(value))
}}
>
<SelectTrigger className='text-foreground w-[64px] font-medium tabular-nums sm:w-[70px]'>
<SelectTrigger className='text-foreground h-8 w-[64px] font-medium tabular-nums sm:w-[70px]'>
<SelectValue placeholder={pageSize} />
</SelectTrigger>
<SelectContent side='top' alignItemWithTrigger={false}>
@@ -100,9 +100,8 @@ export function DataTablePagination<TData>({
<div className='flex min-w-0 shrink-0 items-center gap-1 @lg/pagination:gap-1.5 @xl/pagination:gap-2'>
<Button
size='icon'
variant='outline'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 @max-lg/pagination:hidden'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0 @max-lg/pagination:hidden'
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
@@ -110,9 +109,8 @@ export function DataTablePagination<TData>({
<DoubleArrowLeftIcon className='h-4 w-4' />
</Button>
<Button
size='icon'
variant='outline'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
@@ -130,7 +128,7 @@ export function DataTablePagination<TData>({
<Button
variant={currentPage === pageNumber ? 'default' : 'outline'}
className={cn(
'min-w-8 px-2 tabular-nums',
'h-8 min-w-8 px-2 tabular-nums',
currentPage === pageNumber
? 'font-semibold'
: 'text-muted-foreground hover:text-foreground'
@@ -147,9 +145,8 @@ export function DataTablePagination<TData>({
))}
<Button
size='icon'
variant='outline'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
@@ -157,9 +154,8 @@ export function DataTablePagination<TData>({
<ChevronRightIcon className='h-4 w-4' />
</Button>
<Button
size='icon'
variant='outline'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 @max-lg/pagination:hidden'
className='text-muted-foreground hover:text-foreground disabled:text-muted-foreground/50 size-8 p-0 @max-lg/pagination:hidden'
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { MoreHorizontal } from 'lucide-react'
import * as React from 'react'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
@@ -42,7 +42,7 @@ export function DataTableRowActionMenu(props: DataTableRowActionMenuProps) {
render={
<Button
variant='ghost'
size='icon-sm'
size='icon'
className='data-popup-open:bg-muted'
aria-label={props.ariaLabel}
/>
+1 -1
View File
@@ -19,7 +19,6 @@ For commercial licensing, please contact support@quantumnous.com
import { Database } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { TableRow, TableCell } from '@/components/design-system/table'
import {
Empty,
EmptyDescription,
@@ -27,6 +26,7 @@ import {
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { TableRow, TableCell } from '@/components/ui/table'
interface TableEmptyProps {
/**
+1 -4
View File
@@ -26,10 +26,7 @@ export function getTableSizeStyle<TData>(
): React.CSSProperties {
const width = table
.getVisibleLeafColumns()
.filter(
(column) =>
!isContentSizedColumn(column.id, column.columnDef.meta?.contentSized)
)
.filter((column) => !isContentSizedColumn(column.id))
.reduce((total, column) => total + column.getSize(), 0)
return {
@@ -18,8 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
*/
import type { Table } from '@tanstack/react-table'
import { TableRow, TableCell } from '@/components/design-system/table'
import { Skeleton } from '@/components/ui/skeleton'
import { TableRow, TableCell } from '@/components/ui/table'
import { cn } from '@/lib/utils'
const SKELETON_WIDTHS = [
@@ -50,7 +50,7 @@ export function TruncatedCell({
return (
<div
className={cn(
'block max-w-full min-w-0 whitespace-normal break-words [overflow-wrap:anywhere]',
'block max-w-full min-w-0 truncate',
cellClassName,
className
)}
@@ -64,8 +64,7 @@ export function TruncatedCell({
<Tooltip>
<TooltipTrigger
render={
<span
tabIndex={0}
<div
className={cn(
'block max-w-full min-w-0 truncate',
cellClassName,
@@ -74,9 +73,7 @@ export function TruncatedCell({
/>
}
>
<span className={cn('block truncate', contentClassName)}>
{children}
</span>
<div className={cn('truncate', contentClassName)}>{children}</div>
</TooltipTrigger>
<TooltipContent
side={side}
-1
View File
@@ -42,7 +42,6 @@ export type DataTableViewProps<TData> = {
rows?: Row<TData>[]
emptyTitle?: string
emptyDescription?: string
scrollLabel?: string
emptyIcon?: React.ReactNode
emptyAction?: React.ReactNode
emptyContent?: React.ReactNode
+1 -19
View File
@@ -20,19 +20,9 @@ export { DataTablePagination } from './core/pagination'
export { DataTableColumnHeader } from './core/column-header'
export { BadgeCell } from './core/badge-cell'
export { BadgeListCell } from './core/badge-list-cell'
export {
BadgeListCellDisplayContext,
type BadgeListCellDisplay,
} from './core/badge-list-cell-context'
export { TruncatedCell } from './core/truncated-cell'
export { DataTableViewOptions } from './toolbar/view-options'
export { DataTableToolbar } from './toolbar/toolbar'
export {
DataTableFilterField,
DataTableFilterInput,
DataTableFilterPanel,
type DataTableFilterPanelProps,
} from './toolbar/filter-panel'
export { DataTableBulkActions } from './toolbar/bulk-actions'
export {
StaticDataTable,
@@ -48,21 +38,13 @@ export {
type DataTablePinnedColumn,
type DataTableRenderRowHelpers,
} from './core/data-table-view'
export {
MobileCardList,
type MobileCardListProps,
} from './layout/mobile-card-list'
export { MobileCardList } from './layout/mobile-card-list'
export {
DataTableCardGrid,
type DataTableCardGridProps,
type DataTableCardHelpers,
} from './layout/card-grid'
export { CardRowContent } from './layout/card-row-content'
export {
DataTableCardField,
DataTableCardRow,
type DataTableContentMode,
} from './layout/card-field'
export { tableHasCompactMeta } from './layout/card-cell-utils'
export {
DataTablePage,
@@ -46,10 +46,14 @@ export function renderCellContent<TData>(
return cell.getValue() as ReactNode
}
/** Whether visible columns declare a title/status card hierarchy. */
/**
* Whether any visible column declares `mobileTitle`/`mobileBadge` meta. When
* true the compact two-tier layout is used; otherwise the condensed
* label:value fallback layout is used.
*/
export function tableHasCompactMeta<TData>(table: Table<TData>): boolean {
return table.getVisibleLeafColumns().some((col) => {
const meta = col.columnDef.meta
return meta?.cardRole === 'title' || meta?.cardRole === 'badge'
return Boolean(meta?.mobileTitle || meta?.mobileBadge)
})
}
@@ -1,119 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { ReactNode } from 'react'
import { cn } from '@/lib/utils'
export type DataTableContentMode = 'full' | 'wrap' | 'summary'
const VALUE_WRAP_CLASS =
'whitespace-normal break-words [overflow-wrap:anywhere] [&_.truncate]:overflow-visible [&_.truncate]:text-clip [&_.truncate]:whitespace-normal'
interface DataTableCardFieldProps {
children: ReactNode
className?: string
contentMode?: DataTableContentMode
label?: ReactNode
span?: 1 | 2
valueClassName?: string
}
/**
* Stacked label-above-value field. Prefer {@link DataTableCardRow} for dense
* scannable cards; keep this for multi-line badge collections.
*/
export function DataTableCardField({
children,
className,
contentMode = 'wrap',
label,
span = 1,
valueClassName,
}: DataTableCardFieldProps) {
return (
<div
data-slot='data-table-card-field'
className={cn('min-w-0', span === 2 && 'col-span-2', className)}
>
{label && (
<div className='text-muted-foreground mb-1.5 text-xs leading-none select-none'>
{label}
</div>
)}
<div
data-slot='data-table-card-value'
className={cn(
'min-w-0 text-sm leading-snug',
(contentMode === 'full' || contentMode === 'wrap') &&
VALUE_WRAP_CLASS,
contentMode === 'full' && 'break-all',
valueClassName
)}
>
{children ?? '-'}
</div>
</div>
)
}
interface DataTableCardRowProps {
children: ReactNode
className?: string
contentMode?: DataTableContentMode
label: ReactNode
valueClassName?: string
}
/**
* Dense definition-list row: muted label left, value right.
* Always visible — no progressive disclosure / "More" click required.
*/
export function DataTableCardRow({
children,
className,
contentMode = 'wrap',
label,
valueClassName,
}: DataTableCardRowProps) {
return (
<div
data-slot='data-table-card-row'
className={cn(
'flex min-h-6 items-start justify-between gap-4 py-0.5',
className
)}
>
<span className='text-muted-foreground shrink-0 pt-0.5 text-xs select-none'>
{label}
</span>
<div
data-slot='data-table-card-value'
className={cn(
'flex min-w-0 flex-wrap items-center justify-end gap-1 text-right text-sm leading-snug',
(contentMode === 'full' || contentMode === 'wrap') &&
VALUE_WRAP_CLASS,
contentMode === 'full' && 'break-all',
valueClassName
)}
>
{children ?? <span className='text-muted-foreground'>-</span>}
</div>
</div>
)
}
+2 -4
View File
@@ -37,7 +37,7 @@ import { CardRowContent } from './card-row-content'
/** Helpers passed to a custom {@link DataTableCardGridProps.renderCard}. */
export type DataTableCardHelpers = {
/**
* Whether the table declares title/status `cardRole` metadata.
* Whether the table declares compact card meta (`mobileTitle`/`mobileBadge`).
* Provided so custom renderers can match the default layout decision.
*/
compact: boolean
@@ -53,7 +53,6 @@ export interface DataTableCardGridProps<TData> {
emptyTitle?: string
emptyDescription?: string
emptyIcon?: React.ReactNode
emptyAction?: React.ReactNode
getRowKey?: (row: Row<TData>) => string | number
getRowClassName?: (row: Row<TData>) => string | undefined
/**
@@ -152,7 +151,6 @@ export function DataTableCardGrid<TData>(props: DataTableCardGridProps<TData>) {
<EmptyTitle>{resolvedEmptyTitle}</EmptyTitle>
<EmptyDescription>{resolvedEmptyDescription}</EmptyDescription>
</EmptyHeader>
{props.emptyAction}
</Empty>
</div>
)
@@ -169,7 +167,7 @@ export function DataTableCardGrid<TData>(props: DataTableCardGridProps<TData>) {
data-slot='data-table-card'
data-state={isSelected ? 'selected' : undefined}
className={cn(
'rounded-lg border bg-(--data-table-card-bg,var(--table-row)) px-3.5 py-3 transition-[background-color,border-color] duration-150 data-[state=selected]:[--data-table-card-bg:color-mix(in_oklch,var(--primary)_7%,var(--table-row))] data-[state=selected]:border-primary/40',
'rounded-lg border bg-(--data-table-card-bg,var(--table-row)) px-3 py-2.5 transition-[background-color,border-color] duration-150 data-[state=selected]:[--data-table-card-bg:color-mix(in_oklch,var(--primary)_7%,var(--table-row))] data-[state=selected]:border-primary/40',
props.getRowClassName?.(row)
)}
>
@@ -17,24 +17,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Cell, Row } from '@tanstack/react-table'
import * as React from 'react'
import { StatusBadgeTypeContext } from '@/components/status-badge'
import { getCellLabel, renderCellContent } from './card-cell-utils'
import { DataTableCardField, DataTableCardRow } from './card-field'
type CardRole = 'title' | 'badge' | 'primary' | 'secondary' | 'hidden'
function getCardRole<TData>(cell: Cell<TData, unknown>): CardRole {
const meta = cell.column.columnDef.meta
if (meta?.cardRole) return meta.cardRole
return 'primary'
}
function orderCardCells<TData>(
cells: Cell<TData, unknown>[]
): Cell<TData, unknown>[] {
return [...cells].sort((a, b) => {
const aOrder = a.column.columnDef.meta?.cardOrder
const bOrder = b.column.columnDef.meta?.cardOrder
const aOrder = a.column.columnDef.meta?.mobileOrder
const bOrder = b.column.columnDef.meta?.mobileOrder
if (aOrder == null && bOrder == null) return 0
if (aOrder == null) return 1
@@ -43,103 +37,178 @@ function orderCardCells<TData>(
})
}
function isWideField<TData>(cell: Cell<TData, unknown>): boolean {
const meta = cell.column.columnDef.meta
return meta?.cardSpan === 2 || meta?.contentMode === 'summary'
}
/**
* Shared, column-meta-driven card content rendering for TanStack rows.
*
* Both {@link MobileCardList} (mobile) and {@link DataTableCardGrid} (desktop
* card view) render the same inner content; only the surrounding container
* differs (single bordered list vs. responsive grid of cards). Keeping the
* per-row content here guarantees the two stay visually consistent.
*
* Column meta extensions (see `card-cell-utils.ts`):
* - `mobileTitle` — card header (left, larger text)
* - `mobileBadge` — inline with title (right, e.g. status badge)
* - `mobileHidden` — hidden in card content
*/
/**
* Shared row content for both the mobile list and optional desktop card grid.
* All visible fields render immediately — no "More" click to reveal content.
* Compact content — structured layout with title header + side-by-side fields.
* Used when columns define mobileTitle or mobileBadge meta.
*
* Visual structure:
* [Title content] [Badge]
* [Field1 label] [Field2 label]
* [Field1 value] [Field2 value]
* [Actions ⋯]
*/
export function CardRowContent<TData>(props: {
row: Row<TData>
compact: boolean
}) {
const cells = props.row
function CompactContent<TData>({ row }: { row: Row<TData> }) {
const allCells = row
.getVisibleCells()
.filter((cell) => cell.column.id !== 'select')
const titleCell = cells.find((cell) => getCardRole(cell) === 'title')
const badgeCell = cells.find((cell) => getCardRole(cell) === 'badge')
const actionsCell = cells.find((cell) => cell.column.id === 'actions')
const bodyCells = orderCardCells(
cells.filter(
(cell) =>
cell !== titleCell &&
cell !== badgeCell &&
cell !== actionsCell &&
getCardRole(cell) !== 'hidden'
// Read each cell's meta once, then reuse for all categorisation checks.
const cellMetas = React.useMemo(
() => allCells.map((c) => c.column.columnDef.meta),
// eslint-disable-next-line react-hooks/exhaustive-deps
[allCells.map((c) => c.id).join(',')]
)
const titleCell = allCells.find((_, i) => cellMetas[i]?.mobileTitle)
const badgeCell = allCells.find((_, i) => cellMetas[i]?.mobileBadge)
const actionsCell = allCells.find((c) => c.column.id === 'actions')
const fieldCells = orderCardCells(
allCells.filter(
(c, i) =>
c !== titleCell &&
c !== badgeCell &&
c !== actionsCell &&
!cellMetas[i]?.mobileHidden
)
)
const rowCells = bodyCells.filter((cell) => !isWideField(cell))
const wideCells = bodyCells.filter((cell) => isWideField(cell))
return (
<div className='flex min-w-0 flex-col'>
{props.compact && (titleCell || badgeCell) && (
<div className='flex min-w-0 flex-wrap items-start justify-between gap-3'>
<div className='min-w-0 flex-1 text-[15px] leading-tight font-semibold [overflow-wrap:anywhere] break-words whitespace-normal [&_.truncate]:overflow-visible [&_.truncate]:text-clip [&_.truncate]:whitespace-normal'>
{titleCell ? renderCellContent(titleCell) : null}
<>
{/* Row 1: Title + Badge */}
<div className='flex items-center justify-between gap-2'>
{titleCell && (
<div className='min-w-0 flex-1 text-sm font-medium [&_[data-slot=status-badge]]:max-w-full [&_[data-slot=status-badge]]:whitespace-normal'>
{renderCellContent(titleCell)}
</div>
{badgeCell && (
<div className='shrink-0 text-right'>
{renderCellContent(badgeCell)}
</div>
)}
</div>
)}
)}
{badgeCell && (
<div className='flex-none [&_[data-slot=status-badge]]:max-w-none'>
{renderCellContent(badgeCell)}
</div>
)}
</div>
{!props.compact && (
<div className='grid grid-cols-2 gap-x-3 gap-y-2'>
{bodyCells.map((cell) => {
const meta = cell.column.columnDef.meta
{/* Row 2: Key fields wrap into compact columns instead of squeezing */}
{fieldCells.length > 0 && (
<div className='mt-1.5 grid grid-cols-2 gap-x-3 gap-y-1.5'>
{fieldCells.map((cell) => {
const label = getCellLabel(cell)
return (
<DataTableCardField
key={cell.id}
label={getCellLabel(cell)}
contentMode={meta?.contentMode}
span={meta?.cardSpan}
>
{renderCellContent(cell)}
</DataTableCardField>
<div key={cell.id} className='min-w-0 flex-1 overflow-hidden'>
{label && (
<div className='text-muted-foreground mb-0.5 text-[10px] leading-none select-none'>
{label}
</div>
)}
<div className='min-w-0 overflow-hidden text-xs'>
<StatusBadgeTypeContext.Provider value='text'>
{renderCellContent(cell) ?? '-'}
</StatusBadgeTypeContext.Provider>
</div>
</div>
)
})}
</div>
)}
{props.compact && rowCells.length > 0 && (
<div className='mt-3 space-y-0.5 border-t pt-3'>
{rowCells.map((cell) => (
<DataTableCardRow
key={cell.id}
label={getCellLabel(cell)}
contentMode={cell.column.columnDef.meta?.contentMode}
>
{renderCellContent(cell)}
</DataTableCardRow>
))}
</div>
)}
{props.compact && wideCells.length > 0 && (
<div className='mt-3 space-y-3 border-t pt-3'>
{wideCells.map((cell) => (
<DataTableCardField
key={cell.id}
label={getCellLabel(cell)}
contentMode={cell.column.columnDef.meta?.contentMode ?? 'full'}
>
{renderCellContent(cell)}
</DataTableCardField>
))}
</div>
)}
{/* Actions */}
{actionsCell && (
<div className='mt-3 flex justify-end border-t pt-2'>
<div className='mt-1 -mb-0.5 flex justify-end'>
{renderCellContent(actionsCell)}
</div>
)}
</div>
</>
)
}
/**
* Fallback content — condensed label:value pairs for tables without
* mobileTitle/mobileBadge. Still respects mobileHidden.
*/
function FallbackContent<TData>({ row }: { row: Row<TData> }) {
const allCells = row
.getVisibleCells()
.filter((cell) => cell.column.id !== 'select')
const cellMetas = React.useMemo(
() => allCells.map((c) => c.column.columnDef.meta),
// eslint-disable-next-line react-hooks/exhaustive-deps
[allCells.map((c) => c.id).join(',')]
)
const actionsCell = allCells.find((c) => c.column.id === 'actions')
const contentCells = orderCardCells(
allCells.filter(
(c, i) => c.column.id !== 'actions' && !cellMetas[i]?.mobileHidden
)
)
return (
<>
{contentCells.map((cell) => {
const label = getCellLabel(cell)
if (!label) {
return (
<div key={cell.id} className='flex justify-end overflow-hidden'>
<StatusBadgeTypeContext.Provider value='text'>
{renderCellContent(cell)}
</StatusBadgeTypeContext.Provider>
</div>
)
}
return (
<div
key={cell.id}
className='flex items-start justify-between gap-2 overflow-hidden'
>
<span className='text-muted-foreground shrink-0 text-[10px] font-medium select-none'>
{label}
</span>
<div className='flex min-w-0 flex-1 items-center justify-end overflow-hidden text-xs'>
<StatusBadgeTypeContext.Provider value='text'>
{renderCellContent(cell) ?? '-'}
</StatusBadgeTypeContext.Provider>
</div>
</div>
)
})}
{actionsCell && (
<div className='-mb-0.5 flex justify-end pt-0.5'>
{renderCellContent(actionsCell)}
</div>
)}
</>
)
}
/**
* Renders a single row's card content, auto-selecting the compact or fallback
* layout. Callers compute `compact` once per table (via `tableHasCompactMeta`)
* and pass it down to avoid recomputation per row.
*/
export function CardRowContent<TData>(props: {
row: Row<TData>
compact: boolean
}) {
return props.compact ? (
<CompactContent row={props.row} />
) : (
<FallbackContent row={props.row} />
)
}
@@ -64,11 +64,6 @@ export type DataTablePageProps<TData> = {
*/
columns: ColumnDef<TData, unknown>[]
/**
* Localized accessible name for the keyboard-scrollable table region.
*/
tableLabel?: string
/**
* Initial loading state — renders {@link TableSkeleton} or mobile skeleton.
*/
@@ -324,7 +319,6 @@ export function DataTablePage<TData>(props: DataTablePageProps<TData>) {
<>
<div
className={cn(
'w-full min-w-0',
props.fixedHeight !== false
? 'flex h-full min-h-0 flex-col gap-2.5 sm:gap-3'
: 'space-y-2.5 sm:space-y-3',
@@ -412,7 +406,6 @@ function renderMobile<TData>(
isLoading={props.isLoading}
emptyTitle={props.emptyTitle}
emptyDescription={props.emptyDescription}
scrollLabel={props.tableLabel}
emptyIcon={props.emptyIcon}
emptyAction={props.emptyAction}
skeletonKeyPrefix={props.skeletonKeyPrefix}
@@ -424,7 +417,11 @@ function renderMobile<TData>(
)}
getColumnClassName={props.getColumnClassName}
pinnedColumns={props.pinnedColumns}
containerClassName={props.tableClassName}
containerClassName={cn(
'transition-opacity duration-150',
isFetchingOnly && 'pointer-events-none opacity-60',
props.tableClassName
)}
getRowClassName={(row) =>
props.getRowClassName?.(row, { isMobile: false })
}
@@ -438,7 +435,6 @@ function renderMobile<TData>(
emptyTitle={props.emptyTitle}
emptyDescription={props.emptyDescription}
emptyIcon={props.emptyIcon}
emptyAction={props.emptyAction}
renderCard={props.renderCard}
gridClassName={props.cardGridClassName ?? 'grid grid-cols-1 gap-3'}
skeletonKeyPrefix={props.skeletonKeyPrefix}
@@ -453,8 +449,6 @@ function renderMobile<TData>(
isLoading={props.isLoading}
emptyTitle={props.emptyTitle}
emptyDescription={props.emptyDescription}
emptyIcon={props.emptyIcon}
emptyAction={props.emptyAction}
getRowKey={props.mobileProps?.getRowKey}
getRowClassName={mobileGetRowClassName}
/>
@@ -462,17 +456,7 @@ function renderMobile<TData>(
}
}
return (
<div
aria-busy={props.isLoading || props.isFetching || undefined}
className={cn(
'min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto transition-opacity duration-150',
isFetchingOnly && 'opacity-60'
)}
>
{mobileContent}
</div>
)
return <div className='min-h-0 flex-1 overflow-y-auto'>{mobileContent}</div>
}
function renderDesktop<TData>(
@@ -494,9 +478,8 @@ function renderDesktop<TData>(
className={cn(
fixedHeight && 'min-h-0 flex-1 overflow-y-auto',
'transition-opacity duration-150',
isFetchingOnly && 'opacity-60'
isFetchingOnly && 'pointer-events-none opacity-60'
)}
aria-busy={props.isLoading || props.isFetching || undefined}
>
<DataTableCardGrid
table={props.table}
@@ -504,7 +487,6 @@ function renderDesktop<TData>(
emptyTitle={props.emptyTitle}
emptyDescription={props.emptyDescription}
emptyIcon={props.emptyIcon}
emptyAction={props.emptyAction}
renderCard={props.renderCard}
gridClassName={props.cardGridClassName}
skeletonKeyPrefix={props.skeletonKeyPrefix}
@@ -522,7 +504,6 @@ function renderDesktop<TData>(
isLoading={props.isLoading}
emptyTitle={props.emptyTitle}
emptyDescription={props.emptyDescription}
scrollLabel={props.tableLabel}
emptyIcon={props.emptyIcon}
emptyAction={props.emptyAction}
skeletonKeyPrefix={props.skeletonKeyPrefix}
@@ -539,12 +520,9 @@ function renderDesktop<TData>(
containerClassName={cn(
fixedHeight && 'min-h-0 flex-1',
'transition-opacity duration-150',
isFetchingOnly && 'opacity-60',
isFetchingOnly && 'pointer-events-none opacity-60',
props.tableClassName
)}
containerProps={{
'aria-busy': props.isLoading || props.isFetching || undefined,
}}
getRowClassName={(row) =>
props.getRowClassName?.(row, { isMobile: false })
}
@@ -34,16 +34,13 @@ import { cn } from '@/lib/utils'
import { tableHasCompactMeta } from './card-cell-utils'
import { CardRowContent } from './card-row-content'
export interface MobileCardListProps<TData> {
interface MobileCardListProps<TData> {
table: Table<TData>
isLoading?: boolean
emptyTitle?: string
emptyDescription?: string
emptyIcon?: React.ReactNode
emptyAction?: React.ReactNode
getRowKey?: (row: Row<TData>) => string | number
getRowClassName?: (row: Row<TData>) => string | undefined
renderCard?: (row: Row<TData>) => React.ReactNode
}
function ListSkeleton() {
@@ -96,7 +93,7 @@ function FallbackListSkeleton() {
*
* Per-row content is shared with the desktop card view via
* {@link CardRowContent}; see `card-row-content.tsx` for the column-meta
* extensions (`cardRole`, `cardOrder`, `cardSpan`, `contentMode`).
* extensions (`mobileTitle`, `mobileBadge`, `mobileHidden`).
*/
export function MobileCardList<TData>(props: MobileCardListProps<TData>) {
const {
@@ -131,34 +128,29 @@ export function MobileCardList<TData>(props: MobileCardListProps<TData>) {
<Empty className='border-none p-0'>
<EmptyHeader>
<EmptyMedia variant='icon'>
{props.emptyIcon ?? <Database className='size-6' />}
<Database className='size-6' />
</EmptyMedia>
<EmptyTitle>{resolvedEmptyTitle}</EmptyTitle>
<EmptyDescription>{resolvedEmptyDescription}</EmptyDescription>
</EmptyHeader>
{props.emptyAction}
</Empty>
</div>
)
}
return (
<div className='min-w-0 divide-y overflow-hidden rounded-lg border'>
<div className='divide-y overflow-hidden rounded-lg border'>
{rows.map((row) => {
const key = getRowKey ? getRowKey(row) : row.id
return (
<div
key={key}
className={cn(
'[background-color:var(--data-table-card-bg,var(--table-row))] px-3.5 py-3',
'[background-color:var(--data-table-card-bg,var(--table-row))] px-3 py-2.5',
getRowClassName?.(row)
)}
>
{props.renderCard ? (
props.renderCard(row)
) : (
<CardRowContent row={row} compact={hasCompactMeta} />
)}
<CardRowContent row={row} compact={hasCompactMeta} />
</div>
)
})}
@@ -24,9 +24,10 @@ export const staticDataTableClassNames = {
compactHeaderRow: 'hover:bg-transparent',
mutedHeaderRow:
'[background-color:var(--table-header)] hover:[background-color:var(--table-header-hover)]',
compactHeaderCell: 'text-muted-foreground py-2 text-xs font-medium',
compactHeaderCell:
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase',
compactHeaderCellRight:
'text-muted-foreground py-2 text-right text-xs font-medium',
'text-muted-foreground py-2 text-right text-[10px] font-medium tracking-wider uppercase',
compactCell: 'py-2.5',
compactTopCell: 'py-2.5 align-top',
compactTopNumericCell: 'py-2.5 text-right align-top font-mono',
@@ -25,7 +25,7 @@ import {
TableHead,
TableHeader,
TableRow,
} from '@/components/design-system/table'
} from '@/components/ui/table'
import { cn } from '@/lib/utils'
import { TruncatedCell } from '../core/truncated-cell'
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { Pencil, Trash2 } from 'lucide-react'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
DropdownMenuItem,
DropdownMenuShortcut,
@@ -21,8 +21,8 @@ import { X } from 'lucide-react'
import { useState, useEffect, useLayoutEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import {
Tooltip,
@@ -184,8 +184,9 @@ export function DataTableBulkActions<TData>({
render={
<Button
variant='outline'
size='icon-xs'
size='icon'
onClick={handleClearSelection}
className='size-6'
aria-label={t('Clear selection')}
title={t('Clear selection (Escape)')}
/>
@@ -16,12 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Column } from '@tanstack/react-table'
import { Check as CheckIcon, ChevronDown } from 'lucide-react'
import type { ComponentType, ReactNode } from 'react'
import { type Column } from '@tanstack/react-table'
import { Check as CheckIcon, PlusCircle as PlusCircledIcon } from 'lucide-react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
@@ -30,12 +31,13 @@ import {
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/design-system/command'
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
type DataTableFacetedFilterProps<TData, TValue> = {
@@ -44,15 +46,15 @@ type DataTableFacetedFilterProps<TData, TValue> = {
options: {
label: string
value: string
icon?: ComponentType<{ className?: string }>
iconNode?: ReactNode
icon?: React.ComponentType<{ className?: string }>
iconNode?: React.ReactNode
count?: number
}[]
/** Enable single select mode (only one option can be selected at a time) */
singleSelect?: boolean
}
export function DataTableFacetedFilter<TData, TValue>({
function DataTableFacetedFilterInner<TData, TValue>({
column,
title,
options,
@@ -62,21 +64,6 @@ export function DataTableFacetedFilter<TData, TValue>({
const facets = column?.getFacetedUniqueValues()
const filterValue = column?.getFilterValue() as string[] | undefined
const selectedValues = new Set(filterValue)
const resolvedTitle = title ?? t('Filter')
const selectedOptions = options.filter((option) =>
selectedValues.has(option.value)
)
let selectedSummary: string | null = null
if (selectedValues.size === 1 && selectedOptions.length === 1) {
selectedSummary = t(selectedOptions[0].label)
} else if (selectedValues.size === 2 && selectedOptions.length === 2) {
selectedSummary = selectedOptions
.map((option) => t(option.label))
.join(', ')
} else if (selectedValues.size > 0) {
selectedSummary = `${selectedValues.size} ${t('selected')}`
}
const handleOptionSelect = (optionValue: string) => {
const nextSelectedValues = getNextSelectedValues(
@@ -94,69 +81,57 @@ export function DataTableFacetedFilter<TData, TValue>({
<Popover>
<PopoverTrigger
render={
<Button
variant='outline'
className='w-full min-w-0 justify-start font-normal'
aria-label={
selectedSummary
? `${resolvedTitle}: ${selectedSummary}`
: resolvedTitle
}
/>
<Button variant='outline' size='sm' className='h-8 border-dashed' />
}
>
<span className='text-muted-foreground min-w-0 truncate text-left'>
{resolvedTitle}
</span>
{selectedSummary && (
<PlusCircledIcon className='size-4' />
{title}
{selectedValues?.size > 0 && (
<>
<span className='text-muted-foreground' aria-hidden='true'>
:
</span>
<span className='text-foreground min-w-0 flex-1 truncate text-left'>
{selectedSummary}
</span>
<Separator orientation='vertical' className='mx-2 h-4' />
<Badge
variant='secondary'
className='rounded-sm px-1 font-normal lg:hidden'
>
{selectedValues.size}
</Badge>
<div className='hidden space-x-1 lg:flex'>
{selectedValues.size > 2 ? (
<Badge
variant='secondary'
className='rounded-sm px-1 font-normal'
>
{selectedValues.size} {t('selected')}
</Badge>
) : (
options
.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
variant='secondary'
key={option.value}
className='rounded-sm px-1 font-normal'
>
{t(option.label)}
</Badge>
))
)}
</div>
</>
)}
<ChevronDown className='text-muted-foreground ms-auto size-3.5 shrink-0 transition-transform duration-150 group-data-[popup-open]/button:rotate-180' />
</PopoverTrigger>
<PopoverContent
className='w-(--anchor-width) max-w-[360px] min-w-52 p-0'
align='start'
>
<PopoverContent className='max-w-[360px] min-w-[200px] p-0' align='start'>
<Command>
<CommandInput
placeholder={resolvedTitle}
aria-label={resolvedTitle}
/>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>{t('No results found.')}</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = selectedValues.has(option.value)
let optionIcon: ReactNode = null
if (option.iconNode) {
optionIcon = (
<span className='text-muted-foreground flex size-4 items-center justify-center'>
{option.iconNode}
</span>
)
} else if (option.icon) {
optionIcon = (
<option.icon className='text-muted-foreground size-4' />
)
}
const optionCount =
typeof option.count === 'number'
? option.count
: facets?.get(option.value)
return (
<CommandItem
key={option.value}
onSelect={() => handleOptionSelect(option.value)}
aria-selected={isSelected}
>
<div
className={cn(
@@ -166,23 +141,30 @@ export function DataTableFacetedFilter<TData, TValue>({
: 'opacity-50 [&_svg]:invisible'
)}
>
<CheckIcon
className={cn('text-background h-4 w-4')}
aria-hidden='true'
/>
<CheckIcon className={cn('text-background h-4 w-4')} />
</div>
{optionIcon}
{option.iconNode ? (
<span className='text-muted-foreground flex size-4 items-center justify-center'>
{option.iconNode}
</span>
) : option.icon ? (
<option.icon className='text-muted-foreground size-4' />
) : null}
<span
className='min-w-0 flex-1 truncate'
title={t(option.label)}
>
{t(option.label)}
</span>
{optionCount != null && (
<span className='text-muted-foreground ms-auto flex h-4 min-w-4 items-center justify-center text-xs tabular-nums'>
{optionCount}
{typeof option.count === 'number' ? (
<span className='text-muted-foreground ms-auto flex h-4 min-w-4 items-center justify-center font-mono text-xs'>
{option.count}
</span>
)}
) : facets?.get(option.value) ? (
<span className='ms-auto flex h-4 w-4 items-center justify-center font-mono text-xs'>
{facets.get(option.value)}
</span>
) : null}
</CommandItem>
)
})}
@@ -207,6 +189,10 @@ export function DataTableFacetedFilter<TData, TValue>({
)
}
export const DataTableFacetedFilter = React.memo(
DataTableFacetedFilterInner
) as typeof DataTableFacetedFilterInner
function getNextSelectedValues(
selectedValues: Set<string>,
optionValue: string,
@@ -223,5 +209,5 @@ function getNextSelectedValues(
nextSelectedValues.add(optionValue)
}
return [...nextSelectedValues]
return Array.from(nextSelectedValues)
}
@@ -1,314 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Table } from '@tanstack/react-table'
import { ChevronDown, Loader2 } from 'lucide-react'
import { useState, type ComponentProps, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Input } from '@/components/design-system/input'
import { Badge } from '@/components/ui/badge'
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer'
import { useMediaQuery } from '@/hooks'
import { cn } from '@/lib/utils'
import { DataTableViewOptions } from './view-options'
export interface DataTableFilterPanelProps<TData> {
table: Table<TData>
primaryFilters: ReactNode
advancedFilters?: ReactNode
mobilePinnedFilters?: ReactNode
mobileFilters?: ReactNode
mobileFilterCount?: number
stats?: ReactNode
actionStart?: ReactNode
viewToggle?: ReactNode
hideViewOptions?: boolean
hasActiveFilters: boolean
hasAdvancedActiveFilters?: boolean
advancedFilterCount?: number
searchLoading?: boolean
onReset: () => void
onSearch?: () => void
inlineActions?: boolean
className?: string
}
interface DataTableFilterFieldProps {
children: ReactNode
wide?: boolean
className?: string
}
export function DataTableFilterField(props: DataTableFilterFieldProps) {
return (
<div
className={cn(
'min-w-0 [&>button]:w-full [&>input]:w-full [&_[data-slot=select-trigger]]:w-full [&_[data-slot=select-trigger]]:text-sm [&_[data-slot=select-value]]:leading-5',
props.wide && 'sm:col-span-2',
props.className
)}
>
{props.children}
</div>
)
}
export function DataTableFilterInput(props: ComponentProps<typeof Input>) {
return (
<Input
{...props}
className={cn('min-w-0 text-sm leading-5', props.className)}
/>
)
}
export function DataTableFilterPanel<TData>(
props: DataTableFilterPanelProps<TData>
) {
const { t } = useTranslation()
const [advancedOpen, setAdvancedOpen] = useState(false)
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false)
const isMobile = useMediaQuery('(max-width: 640px)')
const hasAdvancedFilters = props.advancedFilters != null
const hasMobileFilters =
props.mobileFilters != null || props.advancedFilters != null
const activeAdvancedCount =
props.advancedFilterCount ?? (props.hasAdvancedActiveFilters ? 1 : 0)
const activeMobileFilterCount = props.mobileFilterCount ?? activeAdvancedCount
const handleMobileReset = () => {
props.onReset()
setMobileFiltersOpen(false)
}
const handleMobileSubmit = () => {
props.onSearch?.()
setMobileFiltersOpen(false)
}
const advancedToggle = hasAdvancedFilters ? (
<Button
type='button'
variant='ghost'
onClick={() => setAdvancedOpen((open) => !open)}
aria-expanded={advancedOpen}
className={cn(
'text-muted-foreground hover:text-foreground gap-1 px-2',
props.hasAdvancedActiveFilters &&
!advancedOpen &&
'text-primary hover:text-primary'
)}
>
{advancedOpen ? t('Collapse') : t('Expand')}
{activeAdvancedCount > 0 && (
<Badge variant='secondary' className='ml-0.5'>
{activeAdvancedCount}
</Badge>
)}
<ChevronDown
className={cn(
'size-3.5 transition-transform duration-200',
advancedOpen && 'rotate-180'
)}
/>
</Button>
) : null
const viewOptions = !props.hideViewOptions ? (
<DataTableViewOptions table={props.table} />
) : null
const desktopActions = (
<div className='ms-auto flex shrink-0 flex-wrap items-center justify-end gap-1.5 sm:gap-2'>
{props.actionStart}
<Button
type='button'
variant={props.onSearch ? 'outline' : 'ghost'}
onClick={props.onReset}
disabled={!props.hasActiveFilters}
className={cn(
!props.onSearch && 'text-muted-foreground hover:text-foreground px-2'
)}
>
{t('Reset')}
</Button>
{props.onSearch && (
<Button
type='button'
onClick={props.onSearch}
disabled={props.searchLoading}
>
{props.searchLoading && <Loader2 className='animate-spin' />}
{t('Search')}
</Button>
)}
{props.viewToggle}
{viewOptions}
</div>
)
if (isMobile && props.mobilePinnedFilters != null) {
return (
<Drawer open={mobileFiltersOpen} onOpenChange={setMobileFiltersOpen}>
<div
className={cn(
'bg-card/50 min-w-0 rounded-lg border p-2.5',
props.className
)}
>
<div className='grid min-w-0 gap-2'>{props.mobilePinnedFilters}</div>
<div className='mt-2 flex min-w-0 flex-col gap-2'>
{props.stats}
<div className='flex flex-wrap items-center justify-end gap-1.5'>
{props.actionStart}
{hasMobileFilters && (
<DrawerTrigger
render={
<Button
type='button'
variant='ghost'
className={cn(
'text-muted-foreground hover:text-foreground gap-1 px-2',
activeMobileFilterCount > 0 &&
'text-primary hover:text-primary'
)}
>
{t('Filter')}
{activeMobileFilterCount > 0 && (
<Badge variant='secondary' className='ml-0.5'>
{activeMobileFilterCount}
</Badge>
)}
</Button>
}
/>
)}
{!hasMobileFilters && (
<Button
type='button'
variant='ghost'
onClick={props.onReset}
disabled={!props.hasActiveFilters}
className='text-muted-foreground hover:text-foreground px-2'
>
{t('Reset')}
</Button>
)}
{props.onSearch && (
<Button
type='button'
onClick={props.onSearch}
disabled={props.searchLoading}
>
{props.searchLoading && <Loader2 className='animate-spin' />}
{t('Search')}
</Button>
)}
{props.viewToggle}
{viewOptions}
</div>
</div>
</div>
{hasMobileFilters && (
<DrawerContent className='max-h-[85dvh] p-0'>
<div className='mx-auto flex w-full max-w-md flex-1 flex-col overflow-hidden'>
<DrawerHeader className='border-border/70 border-b px-4 py-3 text-left'>
<DrawerTitle>{t('Filter')}</DrawerTitle>
<DrawerDescription>{t('Filters')}</DrawerDescription>
</DrawerHeader>
<div className='flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-4 py-3'>
{props.mobileFilters ?? (
<>
{props.primaryFilters}
{props.advancedFilters}
</>
)}
</div>
<DrawerFooter className='border-border/70 grid grid-cols-2 gap-2 border-t px-4 py-3'>
<Button
type='button'
variant='outline'
onClick={handleMobileReset}
disabled={!props.hasActiveFilters}
>
{t('Reset')}
</Button>
<Button
type='button'
onClick={handleMobileSubmit}
disabled={props.searchLoading}
>
{props.searchLoading && <Loader2 className='animate-spin' />}
{props.onSearch ? t('Search') : t('Done')}
</Button>
</DrawerFooter>
</div>
</DrawerContent>
)}
</Drawer>
)
}
return (
<div
className={cn(
'bg-card/50 min-w-0 rounded-lg border p-2.5 sm:p-3',
props.className
)}
>
<div className='flex min-w-0 flex-wrap items-start gap-2'>
<div className='grid min-w-0 flex-1 grid-cols-1 gap-2 sm:grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] [&>input]:w-full'>
{props.primaryFilters}
</div>
{advancedToggle && (
<div className='flex shrink-0 items-center justify-end'>
{advancedToggle}
</div>
)}
{props.inlineActions && desktopActions}
</div>
{advancedOpen && props.advancedFilters && (
<div className='mt-2 grid grid-cols-1 gap-2 sm:grid-cols-[repeat(auto-fit,minmax(10rem,1fr))] [&>input]:w-full'>
{props.advancedFilters}
</div>
)}
{(!props.inlineActions || props.stats != null) && (
<div className='mt-2 flex min-w-0 flex-wrap items-center gap-2'>
{props.stats}
{!props.inlineActions && desktopActions}
</div>
)}
</div>
)
}
+135 -67
View File
@@ -17,15 +17,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Table } from '@tanstack/react-table'
import { ChevronDown, Loader2, X as Cross2Icon } from 'lucide-react'
import * as React from 'react'
import { useState, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { Input } from '@/components/design-system/input'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { useDebounce } from '@/hooks'
import { cn } from '@/lib/utils'
import { DataTableFacetedFilter } from './faceted-filter'
import { DataTableFilterField, DataTableFilterPanel } from './filter-panel'
import { DataTableViewOptions } from './view-options'
type FilterDef = {
columnId: string
@@ -150,6 +153,7 @@ export type DataTableToolbarProps<TData> = {
*/
export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
const { t } = useTranslation()
const [expanded, setExpanded] = useState(false)
const [isSearchComposing, setIsSearchComposing] = useState(false)
const filters = props.filters ?? []
@@ -242,29 +246,32 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
const searchInput = (
<Input
placeholder={placeholder}
aria-label={placeholder}
value={searchValue}
onChange={handleSearchChange}
onCompositionStart={handleSearchCompositionStart}
onCompositionEnd={handleSearchCompositionEnd}
className='w-full'
className='w-full sm:w-[200px] lg:w-[240px]'
/>
)
const filterChips = filters.map((filter) => {
const column = props.table.getColumn(filter.columnId)
if (!column) return null
return (
<DataTableFilterField key={filter.columnId}>
<DataTableFacetedFilter
column={column}
title={filter.title}
options={filter.options}
singleSelect={filter.singleSelect}
/>
</DataTableFilterField>
)
})
const filterChips = React.useMemo(
() =>
filters.map((filter) => {
const column = props.table.getColumn(filter.columnId)
if (!column) return null
return (
<DataTableFacetedFilter
key={filter.columnId}
column={column}
title={filter.title}
options={filter.options}
singleSelect={filter.singleSelect}
/>
)
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[props.filters, props.table]
)
const handleReset = () => {
setIsSearchComposing(false)
@@ -274,57 +281,118 @@ export function DataTableToolbar<TData>(props: DataTableToolbarProps<TData>) {
props.onReset?.()
}
const primarySearch =
props.customSearch !== undefined ? props.customSearch : searchInput
const additionalFilterCount =
filters.length + (props.additionalSearch != null ? 1 : 0)
const inlineActions =
additionalFilterCount <= 3 && !hasExpandable && props.leftActions == null
const useWidePrimarySearch = !inlineActions && additionalFilterCount <= 3
const secondaryMobileFilters =
props.additionalSearch != null ||
filterChips.some(Boolean) ||
hasExpandable ? (
<>
{props.additionalSearch}
{filterChips}
{props.expandable}
</>
) : undefined
const nonSearchColumnFilterCount = props.table
.getState()
.columnFilters.filter((filter) => filter.id !== props.searchKey).length
const mobileFilterCount =
nonSearchColumnFilterCount + (props.hasExpandedActiveFilters ? 1 : 0)
// Reset: outline text-only for form mode (always visible, disabled when
// nothing to reset); ghost text + X for filter-as-you-type mode (only
// visible when active filters exist).
let resetButton: ReactNode = null
if (hasSearch) {
resetButton = (
<Button variant='outline' onClick={handleReset} disabled={!isFiltered}>
{t('Reset')}
</Button>
)
} else if (isFiltered) {
resetButton = (
<Button
variant='ghost'
onClick={handleReset}
className='text-muted-foreground hover:text-foreground gap-1 px-2'
>
{t('Reset')}
<Cross2Icon />
</Button>
)
}
return (
<DataTableFilterPanel
table={props.table}
primaryFilters={
<>
<DataTableFilterField wide={useWidePrimarySearch}>
{primarySearch}
</DataTableFilterField>
const searchButton = hasSearch ? (
<Button onClick={props.onSearch} disabled={props.searchLoading}>
{props.searchLoading && <Loader2 className='animate-spin' />}
{t('Search')}
</Button>
) : null
const viewOptionsNode = !props.hideViewOptions ? (
<DataTableViewOptions table={props.table} />
) : null
const viewToggleNode = props.viewToggle ?? null
const expandToggle = hasExpandable ? (
<Button
variant='ghost'
onClick={() => setExpanded((p) => !p)}
aria-expanded={expanded}
className={cn(
'text-muted-foreground hover:text-foreground gap-1 px-2',
props.hasExpandedActiveFilters &&
!expanded &&
'text-primary hover:text-primary'
)}
>
{expanded ? t('Collapse') : t('Expand')}
<ChevronDown
className={cn(
'size-3.5 transition-transform duration-200',
expanded && 'rotate-180'
)}
/>
</Button>
) : null
const hasLeftActions = props.leftActions != null
if (hasLeftActions) {
return (
<div className={cn('flex flex-col gap-2', props.className)}>
<div className='flex flex-wrap items-center gap-2 sm:gap-3'>
{props.customSearch !== undefined ? props.customSearch : searchInput}
{props.additionalSearch}
{filterChips}
</>
}
advancedFilters={props.expandable}
mobilePinnedFilters={primarySearch}
mobileFilters={secondaryMobileFilters}
mobileFilterCount={mobileFilterCount}
stats={props.leftActions}
actionStart={props.preActions}
viewToggle={props.viewToggle}
hideViewOptions={props.hideViewOptions}
hasActiveFilters={isFiltered}
hasAdvancedActiveFilters={props.hasExpandedActiveFilters}
advancedFilterCount={props.hasExpandedActiveFilters ? 1 : 0}
searchLoading={props.searchLoading}
onReset={handleReset}
onSearch={hasSearch ? props.onSearch : undefined}
inlineActions={inlineActions}
className={props.className}
/>
<div className='ms-auto flex shrink-0 items-center gap-1.5 sm:gap-2'>
{expandToggle}
</div>
</div>
{expanded && hasExpandable && (
<div className='flex flex-wrap items-center gap-2 sm:gap-3'>
{props.expandable}
</div>
)}
<div className='flex flex-wrap items-center gap-2 sm:gap-3'>
{props.leftActions}
<div className='ms-auto flex shrink-0 items-center gap-1.5 sm:gap-2'>
{props.preActions}
{resetButton}
{searchButton}
{viewToggleNode}
{viewOptionsNode}
</div>
</div>
</div>
)
}
return (
<div
className={cn(
'flex flex-wrap items-center gap-2 sm:gap-3',
props.className
)}
>
{props.customSearch !== undefined ? props.customSearch : searchInput}
{props.additionalSearch}
{filterChips}
{expanded && hasExpandable && props.expandable}
<div className='ms-auto flex shrink-0 items-center gap-1.5 sm:gap-2'>
{props.preActions}
{resetButton}
{searchButton}
{viewToggleNode}
{viewOptionsNode}
{expandToggle}
</div>
</div>
)
}
@@ -19,12 +19,12 @@ For commercial licensing, please contact support@quantumnous.com
import { Grid2X2, Table2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Tabs, TabsList, TabsTrigger } from '@/components/design-system/tabs'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import {
DATA_TABLE_VIEW_MODES,
@@ -37,6 +37,12 @@ export type DataTableViewModeToggleProps = {
className?: string
}
type Segment = {
value: DataTableViewMode
icon: React.ComponentType<{ className?: string }>
tooltip: string
}
/**
* Reusable icon segmented control for switching a data table between table and
* card views. Shared, accessible version of the local control used by the
@@ -45,42 +51,56 @@ export type DataTableViewModeToggleProps = {
export function DataTableViewModeToggle(props: DataTableViewModeToggleProps) {
const { t } = useTranslation()
const segments: Segment[] = [
{
value: DATA_TABLE_VIEW_MODES.CARD,
icon: Grid2X2,
tooltip: t('Card view'),
},
{
value: DATA_TABLE_VIEW_MODES.TABLE,
icon: Table2,
tooltip: t('Table view'),
},
]
return (
<Tabs
value={props.value}
onValueChange={(value) => props.onChange(value as DataTableViewMode)}
className={props.className}
<div
role='group'
aria-label={t('View mode')}
className={cn(
'bg-muted/60 inline-flex h-8 items-center rounded-lg border p-0.5',
props.className
)}
>
<TabsList aria-label={t('View mode')}>
<Tooltip>
<TooltipTrigger
render={
<TabsTrigger
value={DATA_TABLE_VIEW_MODES.TABLE}
className='px-2'
/>
}
>
<Table2 aria-hidden='true' className='size-3.5' />
<span className='sr-only'>{t('Table view')}</span>
</TooltipTrigger>
<TooltipContent side='bottom'>{t('Table view')}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<TabsTrigger
value={DATA_TABLE_VIEW_MODES.CARD}
className='px-2'
/>
}
>
<Grid2X2 aria-hidden='true' className='size-3.5' />
<span className='sr-only'>{t('Card view')}</span>
</TooltipTrigger>
<TooltipContent side='bottom'>{t('Card view')}</TooltipContent>
</Tooltip>
</TabsList>
</Tabs>
{segments.map((segment) => {
const Icon = segment.icon
const isActive = segment.value === props.value
return (
<Tooltip key={segment.value}>
<TooltipTrigger
render={
<button
type='button'
onClick={() => props.onChange(segment.value)}
aria-pressed={isActive}
className={cn(
'inline-flex h-full w-7 items-center justify-center rounded-md text-xs font-medium transition-all',
isActive
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
<Icon className='size-3.5' />
</button>
}
/>
<TooltipContent side='bottom' className='text-xs'>
{segment.tooltip}
</TooltipContent>
</Tooltip>
)
})}
</div>
)
}
@@ -20,7 +20,7 @@ import { type Table } from '@tanstack/react-table'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
+1 -1
View File
@@ -20,7 +20,7 @@ import { Calendar as CalendarIcon } from 'lucide-react'
import { enUS, fr, ja, ru, vi, zhCN } from 'react-day-picker/locale'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import {
Popover,
+2 -2
View File
@@ -21,9 +21,9 @@ import * as React from 'react'
import { enUS, fr, ja, ru, vi, zhCN } from 'react-day-picker/locale'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Input } from '@/components/design-system/input'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import { Input } from '@/components/ui/input'
import {
Popover,
PopoverContent,
-42
View File
@@ -1,42 +0,0 @@
# Design-system adapters
This directory is the product-policy boundary above the shadcn source in
`src/components/ui/`.
## Import rule
- Application and feature code imports policy-bearing controls from
`@/components/design-system/*`.
- Files in this directory may import `@/components/ui/*`.
- Do not make `components/ui` depend on this directory.
- Do not proxy a shadcn component unless it carries a stable cross-application
policy. Components without product policy continue to be imported directly
from `components/ui`.
The lint configuration enforces this rule for the managed controls.
## Responsive sizing
Default controls are 28px below the `sm` breakpoint and 32px at `sm` and above.
Dense and micro sizes remain fixed:
- `sm` / `icon-sm`: 28px
- `xs` / `icon-xs`: 24px
- `xl`: 40px below `sm`, 44px at `sm` and above
Call sites should select a semantic size and use `className` only for layout.
They must not pin a managed control to a fixed `h-*` or `size-*` value.
## Updating shadcn
Keep `components.json` pointed at `@/components/ui`. Preview upstream changes
before merging:
```sh
bunx --bun shadcn@latest add <component> --dry-run
bunx --bun shadcn@latest add <component> --diff <file>
```
Merge the upstream source into `components/ui`, then verify that the adapter's
prop forwarding and class overrides still match the upstream API. Never use
`--overwrite` without explicit approval.
@@ -1,80 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'
import * as React from 'react'
import { Button } from '@/components/design-system/button'
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { cn } from '@/lib/utils'
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof Button>) {
return (
<Button
data-slot='alert-dialog-action'
className={cn(className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
variant = 'outline',
size = 'default',
...props
}: AlertDialogPrimitive.Close.Props &
Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
return (
<AlertDialogPrimitive.Close
data-slot='alert-dialog-cancel'
className={cn(className)}
render={<Button variant={variant} size={size} />}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
}
-65
View File
@@ -1,65 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { Button as ShadcnButton } from '@/components/ui/button'
import { cn } from '@/lib/utils'
const responsiveButtonSizeVariants = cva('', {
variants: {
size: {
default:
"h-7 gap-1 px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 sm:h-8 sm:gap-1.5 sm:text-sm sm:has-data-[icon=inline-end]:pr-2 sm:has-data-[icon=inline-start]:pl-2 sm:[&_svg:not([class*='size-'])]:size-4",
xs: null,
sm: null,
lg: 'h-8 sm:h-9',
xl: 'h-10 gap-2 px-4 sm:h-11 sm:px-5',
icon: 'size-7 sm:size-8',
'icon-xs': null,
'icon-sm': null,
'icon-lg': 'size-8 sm:size-9',
},
},
defaultVariants: {
size: 'default',
},
})
type ButtonSize = NonNullable<
VariantProps<typeof responsiveButtonSizeVariants>['size']
>
type ButtonProps = Omit<React.ComponentProps<typeof ShadcnButton>, 'size'> & {
size?: ButtonSize
}
function Button({ className, size = 'default', ...props }: ButtonProps) {
return (
<ShadcnButton
data-control-size={size}
size={size === 'xl' ? 'lg' : size}
className={cn(responsiveButtonSizeVariants({ size }), className)}
{...props}
/>
)
}
export { Button }
export type { ButtonProps, ButtonSize }
@@ -1,37 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import {
ComboboxInput as ShadcnComboboxInput,
type ComboboxInputOption,
} from '@/components/ui/combobox-input'
import { cn } from '@/lib/utils'
function ComboboxInput({
className,
...props
}: React.ComponentProps<typeof ShadcnComboboxInput>) {
return (
<ShadcnComboboxInput className={cn('h-7 sm:h-8', className)} {...props} />
)
}
export { ComboboxInput }
export type { ComboboxInputOption }
-139
View File
@@ -1,139 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Combobox as ComboboxPrimitive } from '@base-ui/react'
import * as React from 'react'
import {
ComboboxInput as LegacyComboboxInput,
type ComboboxInputOption,
} from '@/components/design-system/combobox-input'
import {
ComboboxChip,
ComboboxChips as ShadcnComboboxChips,
ComboboxChipsInput,
ComboboxCollection,
ComboboxContent as ShadcnComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput as ShadcnComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxSeparator,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
} from '@/components/ui/combobox'
import { cn } from '@/lib/utils'
type LegacyComboboxProps = {
options: ComboboxInputOption[]
value?: string
onValueChange?: (value: string | null) => void
placeholder?: string
searchPlaceholder?: string
emptyText?: string
allowCustomValue?: boolean
className?: string
id?: string
openOnFocus?: boolean
}
function Combobox(props: LegacyComboboxProps): React.ReactElement
function Combobox<Value, Multiple extends boolean | undefined = false>(
props: ComboboxPrimitive.Root.Props<Value, Multiple>
): React.ReactElement
function Combobox(
props:
| ComboboxPrimitive.Root.Props<unknown, boolean | undefined>
| LegacyComboboxProps
) {
if ('options' in props) {
return (
<LegacyComboboxInput
id={props.id}
options={props.options}
value={props.value ?? ''}
onValueChange={(value) => props.onValueChange?.(value)}
placeholder={props.searchPlaceholder ?? props.placeholder}
emptyText={props.emptyText}
className={props.className}
allowCustomValue={props.allowCustomValue}
openOnFocus={props.openOnFocus}
/>
)
}
return <ComboboxPrimitive.Root {...props} />
}
function ComboboxInput({
className,
...props
}: React.ComponentProps<typeof ShadcnComboboxInput>) {
return (
<ShadcnComboboxInput className={cn('h-7 sm:h-8', className)} {...props} />
)
}
function ComboboxContent({
className,
...props
}: React.ComponentProps<typeof ShadcnComboboxContent>) {
return (
<ShadcnComboboxContent
className={cn(
'*:data-[slot=input-group]:h-7 sm:*:data-[slot=input-group]:h-8',
className
)}
{...props}
/>
)
}
function ComboboxChips({
className,
...props
}: React.ComponentProps<typeof ShadcnComboboxChips>) {
return (
<ShadcnComboboxChips
className={cn('min-h-7 sm:min-h-8', className)}
{...props}
/>
)
}
export {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
ComboboxSeparator,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
}
-77
View File
@@ -1,77 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { SearchIcon } from '@hugeicons/core-free-icons'
import { HugeiconsIcon } from '@hugeicons/react'
import { Command as CommandPrimitive } from 'cmdk'
import * as React from 'react'
import {
InputGroup,
InputGroupAddon,
} from '@/components/design-system/input-group'
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from '@/components/ui/command'
import { cn } from '@/lib/utils'
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot='command-input-wrapper' className='p-1 pb-0'>
<InputGroup className='border-input/30 bg-input/30 rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!'>
<CommandPrimitive.Input
data-slot='command-input'
className={cn(
'w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
/>
<InputGroupAddon>
<HugeiconsIcon
icon={SearchIcon}
strokeWidth={2}
className='size-4 shrink-0 opacity-50'
/>
</InputGroupAddon>
</InputGroup>
</div>
)
}
export {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
}
-74
View File
@@ -1,74 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
import * as React from 'react'
import { Button } from '@/components/design-system/button'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { cn } from '@/lib/utils'
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<'div'> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot='dialog-footer'
className={cn(
'bg-muted/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t p-4 sm:flex-row sm:justify-end',
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close render={<Button variant='outline' />}>
Close
</DialogPrimitive.Close>
)}
</div>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
@@ -1,79 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import {
InputGroup as ShadcnInputGroup,
InputGroupAddon,
InputGroupButton as ShadcnInputGroupButton,
InputGroupInput as ShadcnInputGroupInput,
InputGroupText,
InputGroupTextarea,
} from '@/components/ui/input-group'
import { cn } from '@/lib/utils'
function InputGroup({
className,
...props
}: React.ComponentProps<typeof ShadcnInputGroup>) {
return (
<ShadcnInputGroup
data-control-size='default'
className={cn('h-7 sm:h-8', className)}
{...props}
/>
)
}
function InputGroupButton({
className,
size = 'xs',
...props
}: React.ComponentProps<typeof ShadcnInputGroupButton>) {
return (
<ShadcnInputGroupButton
data-control-size={size}
size={size}
className={cn(
size === 'sm' && 'h-7 sm:h-8',
size === 'icon-sm' && 'size-7 sm:size-8',
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<typeof ShadcnInputGroupInput>) {
return (
<ShadcnInputGroupInput className={cn('h-full', className)} {...props} />
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
}
-37
View File
@@ -1,37 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import { Input as ShadcnInput } from '@/components/ui/input'
import { cn } from '@/lib/utils'
function Input({
className,
...props
}: React.ComponentProps<typeof ShadcnInput>) {
return (
<ShadcnInput
data-control-size='default'
className={cn('h-7 sm:h-8', className)}
{...props}
/>
)
}
export { Input }
-55
View File
@@ -1,55 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import {
Pagination as ShadcnPagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from '@/components/ui/pagination'
import { cn } from '@/lib/utils'
function Pagination({
className,
...props
}: React.ComponentProps<typeof ShadcnPagination>) {
return (
<ShadcnPagination
className={cn(
'[&_[data-slot=pagination-link]]:h-7 [&_[data-slot=pagination-link]:not(:has(span))]:w-7 [&_[data-slot=pagination-ellipsis]]:size-7 sm:[&_[data-slot=pagination-link]]:h-8 sm:[&_[data-slot=pagination-link]:not(:has(span))]:w-8 sm:[&_[data-slot=pagination-ellipsis]]:size-8',
className
)}
{...props}
/>
)
}
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}
-64
View File
@@ -1,64 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger as ShadcnSelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { cn } from '@/lib/utils'
function SelectTrigger({
className,
size = 'default',
...props
}: React.ComponentProps<typeof ShadcnSelectTrigger>) {
return (
<ShadcnSelectTrigger
data-control-size={size}
size={size}
className={cn(
size === 'default' && 'h-7 text-[0.8rem] sm:h-8 sm:text-sm',
className
)}
{...props}
/>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
-95
View File
@@ -1,95 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput as ShadcnSidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger as ShadcnSidebarTrigger,
useSidebar,
} from '@/components/ui/sidebar'
import { cn } from '@/lib/utils'
function SidebarTrigger({
className,
...props
}: React.ComponentProps<typeof ShadcnSidebarTrigger>) {
return (
<ShadcnSidebarTrigger
className={cn('size-7 sm:size-8', className)}
{...props}
/>
)
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof ShadcnSidebarInput>) {
return (
<ShadcnSidebarInput className={cn('h-7 sm:h-8', className)} {...props} />
)
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}
-49
View File
@@ -1,49 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead as ShadcnTableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { cn } from '@/lib/utils'
function TableHead({
className,
...props
}: React.ComponentProps<typeof ShadcnTableHead>) {
return <ShadcnTableHead className={cn('h-9 sm:h-10', className)} {...props} />
}
export {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
}
-61
View File
@@ -1,61 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import {
Tabs,
TabsContent,
TabsList as ShadcnTabsList,
TabsTrigger as ShadcnTabsTrigger,
} from '@/components/ui/tabs'
import { cn } from '@/lib/utils'
function TabsList({
className,
variant = 'default',
...props
}: React.ComponentProps<typeof ShadcnTabsList>) {
return (
<ShadcnTabsList
data-control-size='default'
variant={variant}
className={cn(
variant === 'line'
? 'group-data-horizontal/tabs:h-auto sm:group-data-horizontal/tabs:h-auto'
: 'group-data-horizontal/tabs:h-7 sm:group-data-horizontal/tabs:h-8',
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof ShadcnTabsTrigger>) {
return (
<ShadcnTabsTrigger
className={cn('text-[0.8rem] sm:text-sm', className)}
{...props}
/>
)
}
export { Tabs, TabsContent, TabsList, TabsTrigger }
@@ -1,46 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import {
ToggleGroup as ShadcnToggleGroup,
ToggleGroupItem,
} from '@/components/ui/toggle-group'
import { cn } from '@/lib/utils'
function ToggleGroup({
className,
size = 'default',
...props
}: React.ComponentProps<typeof ShadcnToggleGroup>) {
return (
<ShadcnToggleGroup
data-control-size={size}
size={size}
className={cn(
size === 'default' &&
"[&_[data-slot=toggle-group-item]]:h-7 [&_[data-slot=toggle-group-item]]:min-w-7 [&_[data-slot=toggle-group-item]]:text-[0.8rem] [&_[data-slot=toggle-group-item]_svg:not([class*='size-'])]:size-3.5 sm:[&_[data-slot=toggle-group-item]]:h-8 sm:[&_[data-slot=toggle-group-item]]:min-w-8 sm:[&_[data-slot=toggle-group-item]]:text-sm sm:[&_[data-slot=toggle-group-item]_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
export { ToggleGroup, ToggleGroupItem }
-59
View File
@@ -1,59 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { Toggle as ShadcnToggle } from '@/components/ui/toggle'
import { cn } from '@/lib/utils'
const responsiveToggleSizeVariants = cva('', {
variants: {
size: {
default:
"h-7 min-w-7 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 sm:h-8 sm:min-w-8 sm:text-sm sm:has-data-[icon=inline-end]:pr-2 sm:has-data-[icon=inline-start]:pl-2 sm:[&_svg:not([class*='size-'])]:size-4",
sm: null,
lg: 'h-8 min-w-8 sm:h-9 sm:min-w-9',
},
},
defaultVariants: {
size: 'default',
},
})
type ToggleSize = NonNullable<
VariantProps<typeof responsiveToggleSizeVariants>['size']
>
type ToggleProps = Omit<React.ComponentProps<typeof ShadcnToggle>, 'size'> & {
size?: ToggleSize
}
function Toggle({ className, size = 'default', ...props }: ToggleProps) {
return (
<ShadcnToggle
data-control-size={size}
size={size}
className={cn(responsiveToggleSizeVariants({ size }), className)}
{...props}
/>
)
}
export { Toggle }
export type { ToggleProps, ToggleSize }
+1 -3
View File
@@ -26,7 +26,7 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/design-system/dialog'
} from '@/components/ui/dialog'
import { cn } from '@/lib/utils'
type DialogProps = React.ComponentProps<typeof DialogRoot> & {
@@ -71,8 +71,6 @@ export function Dialog({
{trigger ? <DialogTrigger render={trigger} /> : null}
<DialogContent
className={cn(
// Default width is sm:max-w-2xl. Override with `sm:max-w-*` in
// contentClassName (bare `max-w-*` will not replace the sm: default).
'flex max-h-[calc(100vh-2rem)] w-full flex-col gap-4 overflow-hidden p-4 sm:max-w-2xl sm:p-6',
contentClassName,
dialogContentMotionClassName
+2 -6
View File
@@ -22,17 +22,13 @@ import { cn } from '@/lib/utils'
export const sideDrawerContentClassName = (className?: string) =>
cn(
// Width: pass `sm:max-w-*` (or `sm:max-w-none`) in className. SheetContent
// defaults to 75% width on mobile and `sm:max-w-sm` for left/right.
// Match its side variants so product drawers occupy the full mobile viewport.
'bg-background text-foreground flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 shadow-none data-[side=left]:w-full data-[side=right]:w-full',
'bg-background text-foreground flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 shadow-none',
className
)
export const sideDrawerHeaderClassName = (className?: string) =>
cn(
// pr-12 reserves space for SheetContent's absolute close button
'border-border/70 bg-background/95 border-b px-4 py-3 pr-12 text-start backdrop-blur supports-[backdrop-filter]:bg-background/80 sm:px-6 sm:py-4 sm:pr-14',
'border-border/70 bg-background/95 border-b px-4 py-3 text-start backdrop-blur supports-[backdrop-filter]:bg-background/80 sm:px-6 sm:py-4',
className
)
+2 -2
View File
@@ -20,8 +20,8 @@ import { AlertTriangle, type LucideIcon } from 'lucide-react'
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { FadeIn } from '@/components/page-transition'
import { Button } from '@/components/ui/button'
import {
Empty,
EmptyContent,
@@ -61,7 +61,7 @@ export function ErrorState(props: ErrorStateProps) {
</EmptyHeader>
<EmptyContent>
{props.onRetry != null && (
<Button variant='outline' onClick={props.onRetry}>
<Button variant='outline' size='sm' onClick={props.onRetry}>
{t('Retry')}
</Button>
)}
+38 -28
View File
@@ -18,23 +18,27 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useTranslation } from 'react-i18next'
import { getIdentityTextColorClass } from '@/lib/colors'
import { cn } from '@/lib/utils'
import { StatusBadge, type StatusBadgeProps } from './status-badge'
type GroupBadgeProps = Omit<StatusBadgeProps, 'children' | 'variant'> & {
type GroupBadgeProps = Omit<
StatusBadgeProps,
'autoColor' | 'label' | 'variant'
> & {
group?: string | null
label?: string
ratio?: number | null
}
function getGroupRatioVariant(
ratio: number
): NonNullable<StatusBadgeProps['variant']> {
if (ratio > 1) return 'warning'
if (ratio < 1) return 'info'
return 'neutral'
function getGroupRatioClassName(ratio: number): string {
if (ratio > 1) {
return 'bg-warning/10 text-warning'
}
if (ratio < 1) {
return 'bg-info/10 text-info'
}
return 'bg-muted text-muted-foreground'
}
function getGroupLabel(params: {
@@ -52,11 +56,19 @@ function getGroupLabel(params: {
export function GroupBadge(props: GroupBadgeProps) {
const { t } = useTranslation()
const { group, label: labelOverride, ratio, className, ...badgeProps } = props
const {
group,
label: labelOverride,
ratio,
copyable = false,
showDot,
className,
...badgeProps
} = props
const groupName = group?.trim()
const isAutoGroup = groupName === 'auto'
const isEmptyGroup = !groupName
const colorKey = groupName || labelOverride || 'group'
const isSpecialGroup = isAutoGroup || isEmptyGroup
const label = getGroupLabel({
labelOverride,
groupName,
@@ -68,16 +80,13 @@ export function GroupBadge(props: GroupBadgeProps) {
const badge = (
<StatusBadge
{...badgeProps}
variant='neutral'
appearance='plain'
className={cn(
'shrink-0 overflow-visible',
getIdentityTextColorClass(colorKey),
className
)}
>
{label}
</StatusBadge>
copyable={copyable}
label={label}
showDot={showDot ?? (isSpecialGroup ? false : undefined)}
variant={isSpecialGroup ? 'neutral' : undefined}
autoColor={isSpecialGroup ? undefined : groupName}
className={cn('min-w-0 shrink overflow-hidden', className)}
/>
)
if (ratio == null) {
@@ -85,15 +94,16 @@ export function GroupBadge(props: GroupBadgeProps) {
}
return (
<span className='inline-flex w-max shrink-0 items-center gap-2 text-xs whitespace-nowrap'>
<span className='inline-flex shrink-0'>{badge}</span>
<StatusBadge
variant={getGroupRatioVariant(ratio)}
appearance='plain'
className='shrink-0 tabular-nums'
<span className='inline-flex max-w-full min-w-0 items-center gap-2 text-xs'>
<span className='max-w-full min-w-0 overflow-hidden'>{badge}</span>
<span
className={cn(
'inline-flex h-5 shrink-0 items-center rounded-full px-1.5 font-mono text-xs leading-none font-medium tabular-nums',
getGroupRatioClassName(ratio)
)}
>
{ratio}x
</StatusBadge>
<span>{ratio}x</span>
</span>
</span>
)
}
+8 -2
View File
@@ -97,7 +97,10 @@ function hardenIsolatedHtml(html: string): string {
template.content.querySelectorAll('a[target="_blank"]').forEach((link) => {
const rel = new Set(
link.getAttribute('rel')?.split(/\s+/).filter(Boolean) ?? []
link
.getAttribute('rel')
?.split(/\s+/)
.filter(Boolean) ?? []
)
rel.add('noopener')
@@ -179,7 +182,10 @@ function IsolatedHtmlContent(props: {
}, [props.html])
return (
<div ref={containerRef} className={cn('block w-full', props.className)} />
<div
ref={containerRef}
className={cn('block w-full', props.className)}
/>
)
}
+4 -3
View File
@@ -26,7 +26,7 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
@@ -229,7 +229,7 @@ export function JsonCodeEditor({
<span
className={cn(
'flex items-center gap-1 text-xs',
jsonStatus.valid ? 'text-success' : 'text-destructive'
jsonStatus.valid ? 'text-emerald-600' : 'text-destructive'
)}
>
{jsonStatus.valid ? (
@@ -242,7 +242,8 @@ export function JsonCodeEditor({
<Button
type='button'
variant='ghost'
size='xs'
size='sm'
className='h-6 px-2 text-xs'
onClick={formatJson}
disabled={disabled || !jsonStatus.valid || !value.trim()}
>
+7 -3
View File
@@ -20,8 +20,8 @@ import { Code, Table, Plus, Trash2 } from 'lucide-react'
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Input } from '@/components/design-system/input'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
@@ -192,6 +192,7 @@ export function JsonEditor({
<Button
type='button'
variant='outline'
size='sm'
onClick={toggleMode}
disabled={disabled}
>
@@ -211,7 +212,8 @@ export function JsonEditor({
<Button
type='button'
variant='link'
className='h-auto p-0 sm:h-auto'
size='sm'
className='h-auto p-0'
onClick={handleFillTemplate}
disabled={disabled}
>
@@ -258,6 +260,7 @@ export function JsonEditor({
aria-label='Delete row'
onClick={() => handleDeleteRow(row.id)}
disabled={disabled}
className='h-10 w-10'
>
<Trash2 className='h-4 w-4' aria-hidden='true' />
</Button>
@@ -272,6 +275,7 @@ export function JsonEditor({
<Button
type='button'
variant='outline'
size='sm'
onClick={handleAddRow}
disabled={disabled}
className='w-full'
+4 -2
View File
@@ -20,7 +20,7 @@ import { Languages, Check } from 'lucide-react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
@@ -55,7 +55,9 @@ export function LanguageSwitcher() {
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger render={<Button variant='ghost' size='icon' />}>
<DropdownMenuTrigger
render={<Button variant='ghost' size='icon' className='h-9 w-9' />}
>
<Languages className='size-[1.2rem]' />
<span className='sr-only'>{t('Change language')}</span>
</DropdownMenuTrigger>
@@ -18,11 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
import {
Sidebar,
SidebarContent,
SidebarRail,
} from '@/components/design-system/sidebar'
import { Sidebar, SidebarContent, SidebarRail } from '@/components/ui/sidebar'
import { useLayout } from '@/context/layout-provider'
import { useSidebarView } from '@/hooks/use-sidebar-view'
import { MOTION_TRANSITION, MOTION_VARIANTS } from '@/lib/motion'
@@ -56,13 +52,7 @@ export function AppSidebar() {
<Sidebar collapsible={collapsible} variant={variant}>
{view && <SidebarViewHeader view={view} />}
<SidebarContent
className={
view
? 'py-2'
: 'py-2 md:pt-[calc(var(--app-header-height,3rem)+0.5rem)]'
}
>
<SidebarContent className='py-2'>
<AnimatePresence mode='wait' initial={false}>
<motion.div
key={key}
@@ -16,12 +16,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import {
SidebarInset,
SidebarProvider,
} from '@/components/design-system/sidebar'
import { AnimatedOutlet } from '@/components/page-transition'
import { SkipToMain } from '@/components/skip-to-main'
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'
import { LayoutProvider } from '@/context/layout-provider'
import { SearchProvider } from '@/context/search-provider'
import { getCookie } from '@/lib/cookies'
@@ -49,7 +46,7 @@ export function AuthenticatedLayout(props: AuthenticatedLayoutProps) {
className={cn(
'@container/content',
'h-[calc(100svh-var(--app-header-height,0px))]',
'min-h-0 min-w-0 overflow-hidden',
'min-h-0 overflow-hidden',
'peer-data-[variant=inset]:h-[calc(100svh-var(--app-header-height,0px)-(var(--spacing)*4))]'
)}
>
@@ -22,14 +22,6 @@ import { useMemo, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import {
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from '@/components/design-system/sidebar'
import {
Collapsible,
CollapsibleContent,
@@ -41,6 +33,14 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from '@/components/ui/sidebar'
import { fetchActiveChatKey } from '@/features/chat/hooks/use-active-chat-key'
import { useChatPresets } from '@/features/chat/hooks/use-chat-presets'
import {
+2 -2
View File
@@ -50,13 +50,13 @@ export function Glow({ className, variant, ...props }: GlowProps) {
>
<div
className={cn(
'from-primary/60 to-primary/0 absolute left-1/2 h-[256px] w-[60%] -translate-x-1/2 scale-[2.5] rounded-[50%] bg-radial from-10% to-60% opacity-40 sm:h-[512px]',
'absolute left-1/2 h-[256px] w-[60%] -translate-x-1/2 scale-[2.5] rounded-[50%] bg-radial from-amber-500/60 from-10% to-amber-500/0 to-60% opacity-40 sm:h-[512px] dark:opacity-80',
variant === 'center' && '-translate-y-1/2'
)}
/>
<div
className={cn(
'from-primary/40 to-primary/0 absolute left-1/2 h-[128px] w-[40%] -translate-x-1/2 scale-200 rounded-[50%] bg-radial from-10% to-60% opacity-30 sm:h-[256px]',
'absolute left-1/2 h-[128px] w-[40%] -translate-x-1/2 scale-200 rounded-[50%] bg-radial from-yellow-400/50 from-10% to-yellow-400/0 to-60% opacity-30 sm:h-[256px] dark:opacity-70',
variant === 'center' && '-translate-y-1/2'
)}
/>
+2 -2
View File
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { SidebarTrigger } from '@/components/design-system/sidebar'
import { SidebarTrigger } from '@/components/ui/sidebar'
import { cn } from '@/lib/utils'
type HeaderProps = React.HTMLAttributes<HTMLElement>
@@ -31,7 +31,7 @@ export function Header({ className, children, ...props }: HeaderProps) {
{...props}
>
<div className='flex h-full items-center gap-1.5 px-2 sm:gap-2 sm:px-3'>
<SidebarTrigger variant='ghost' size='icon' />
<SidebarTrigger variant='ghost' className='size-8' />
{children}
</div>
</header>
@@ -21,9 +21,9 @@ import { X, User, Wallet, LogOut } from 'lucide-react'
import { AnimatePresence, motion, type Variants } from 'motion/react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { SignOutDialog } from '@/components/sign-out-dialog'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import useDialogState from '@/hooks/use-dialog'
import { useUserDisplay } from '@/hooks/use-user-display'
@@ -135,7 +135,7 @@ function MobileUserProfile({ user, onNavigate }: MobileUserProfileProps) {
<Button
variant='ghost'
onClick={() => setSignOutOpen(true)}
className='text-destructive hover:text-destructive/80 h-auto w-full justify-start gap-2.5 p-2.5 hover:bg-transparent sm:h-auto'
className='text-destructive hover:text-destructive/80 h-auto w-full justify-start gap-2.5 p-2.5 hover:bg-transparent'
>
<LogOut className='size-4' />
{t('Sign out')}
@@ -159,7 +159,8 @@ function MobileSignInButton({ onNavigate }: MobileSignInButtonProps) {
return (
<Button
variant='secondary'
className='w-full'
size='sm'
className='h-10 w-full'
render={<Link to='/sign-in' onClick={onNavigate} />}
>
{t('Sign in')}
+12 -12
View File
@@ -20,17 +20,6 @@ import { Link, useLocation } from '@tanstack/react-router'
import { ChevronRight } from 'lucide-react'
import { type ReactNode, useState, useEffect } from 'react'
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from '@/components/design-system/sidebar'
import { Badge } from '@/components/ui/badge'
import {
Collapsible,
@@ -46,6 +35,17 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from '@/components/ui/sidebar'
import { checkIsActive } from '../lib/url-utils'
import {
@@ -66,7 +66,7 @@ export function NavGroup({ title, items }: NavGroupProps) {
return (
<SidebarGroup className='px-2 py-1'>
<SidebarGroupLabel className='text-muted-foreground/70 px-2 text-xs font-medium tracking-wider uppercase'>
<SidebarGroupLabel className='text-muted-foreground/70 px-2 text-[11px] font-medium tracking-wider uppercase'>
{title}
</SidebarGroupLabel>
<SidebarMenu>
@@ -20,12 +20,12 @@ import { Link, useNavigate, useRouterState } from '@tanstack/react-router'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Dialog } from '@/components/dialog'
import { LanguageSwitcher } from '@/components/language-switcher'
import { NotificationPopover } from '@/components/notification-popover'
import { ProfileDropdown } from '@/components/profile-dropdown'
import { ThemeSwitch } from '@/components/theme-switch'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { useNotifications } from '@/hooks/use-notifications'
import { useSystemConfig } from '@/hooks/use-system-config'
@@ -286,7 +286,8 @@ export function PublicHeader(props: PublicHeaderProps) {
<ProfileDropdown />
) : (
<Button
className='font-medium'
size='sm'
className='h-8 rounded-lg px-3.5 text-xs font-medium'
render={<Link to='/sign-in' />}
>
{t('Sign in')}
@@ -306,6 +307,7 @@ export function PublicHeader(props: PublicHeaderProps) {
type='button'
variant='ghost'
size='icon'
className='size-9'
onClick={() => setMobileOpen((v) => !v)}
aria-label={t('Toggle navigation menu')}
>
@@ -1,67 +0,0 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { ReactNode } from 'react'
import { PageTransition } from '@/components/page-transition'
import { cn } from '@/lib/utils'
/** Shared shell for public catalog pages (pricing, rankings, …). */
export const PUBLIC_PAGE_SHELL_CLASS =
'mx-auto w-full max-w-7xl px-4 pt-24 pb-12 sm:px-6 lg:px-8'
export interface PublicPageShellProps {
children: ReactNode
className?: string
}
export function PublicPageShell(props: PublicPageShellProps) {
return (
<PageTransition className={cn(PUBLIC_PAGE_SHELL_CLASS, props.className)}>
{props.children}
</PageTransition>
)
}
export interface PublicPageHeaderProps {
title: ReactNode
description?: ReactNode
/** Full-width slot under the title block (tabs, filters, meta). */
children?: ReactNode
className?: string
}
/**
* Shared page header for public catalog surfaces.
* Title follows the product page-title contract: text-lg / semibold / tight.
*/
export function PublicPageHeader(props: PublicPageHeaderProps) {
return (
<header className={cn('mb-8 space-y-6', props.className)}>
<div className='max-w-3xl'>
<h1 className='text-lg font-semibold tracking-tight'>{props.title}</h1>
{props.description != null && props.description !== '' && (
<p className='text-muted-foreground mt-2 text-sm leading-relaxed'>
{props.description}
</p>
)}
</div>
{props.children}
</header>
)
}
@@ -85,7 +85,7 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
)}
<div className='flex flex-wrap items-center justify-between gap-x-3 gap-y-2 sm:gap-x-4'>
<div className='min-w-0 flex-1'>
<h2 className='truncate text-lg font-semibold tracking-tight'>
<h2 className='truncate text-base font-bold tracking-tight sm:text-lg'>
{title}
</h2>
</div>
@@ -26,7 +26,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/design-system/sidebar'
} from '@/components/ui/sidebar'
import { cn } from '@/lib/utils'
import type { SidebarView } from '../types'
@@ -46,7 +46,7 @@ export function SidebarViewHeader(props: SidebarViewHeaderProps) {
const { setOpenMobile } = useSidebar()
return (
<SidebarHeader className='border-sidebar-border border-b px-2 py-2 md:mt-[var(--app-header-height,3rem)]'>
<SidebarHeader className='border-sidebar-border border-b px-2 py-2'>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
@@ -23,7 +23,7 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/design-system/sidebar'
} from '@/components/ui/sidebar'
import { useStatus } from '@/hooks/use-status'
import { useSystemConfig } from '@/hooks/use-system-config'
import { cn } from '@/lib/utils'
+2 -2
View File
@@ -20,7 +20,7 @@ import { Link } from '@tanstack/react-router'
import { Menu } from 'lucide-react'
import { useMemo } from 'react'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
@@ -58,7 +58,7 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
<div className='lg:hidden'>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
render={<Button size='icon' variant='outline' />}
render={<Button size='icon' variant='outline' className='size-7' />}
>
<Menu />
</DropdownMenuTrigger>
-5
View File
@@ -26,11 +26,6 @@ export { AppSidebar } from './components/app-sidebar'
export { AuthenticatedLayout } from './components/authenticated-layout'
export { PublicLayout } from './components/public-layout'
export { PublicHeader } from './components/public-header'
export {
PublicPageHeader,
PublicPageShell,
PUBLIC_PAGE_SHELL_CLASS,
} from './components/public-page-header'
export { PublicNavigation } from './components/public-navigation'
export { HeaderLogo } from './components/header-logo'
export { NavLinkItem, NavLinkList } from './components/nav-link-item'
+1 -1
View File
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { CircleQuestionMark } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
+3 -3
View File
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { CopyButton } from '@/components/copy-button'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
@@ -49,7 +49,7 @@ export function MaskedValueDisplay(props: MaskedValueDisplayProps) {
<Button
variant='ghost'
size='sm'
className='max-w-full min-w-0 justify-start truncate px-0 font-mono hover:bg-transparent aria-expanded:bg-transparent'
className='h-7 max-w-full min-w-0 justify-start truncate px-0 font-mono hover:bg-transparent aria-expanded:bg-transparent'
/>
}
>
@@ -72,7 +72,7 @@ export function MaskedValueDisplay(props: MaskedValueDisplayProps) {
</Popover>
<CopyButton
value={props.fullValue}
size='icon-sm'
className='size-7'
iconClassName='size-3.5'
tooltip={props.copyTooltip}
aria-label={props.copyAriaLabel}
+40 -38
View File
@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
@@ -28,7 +28,7 @@ import {
CommandInput,
CommandItem,
CommandList,
} from '@/components/design-system/command'
} from '@/components/ui/command'
import {
Drawer,
DrawerContent,
@@ -92,11 +92,12 @@ const ModelTriggerButton = React.forwardRef<
ref={ref}
variant='outline'
role='combobox'
size='sm'
disabled={isDisabled}
className={cn(
'flex items-center gap-2 border px-3 font-medium',
'flex h-8 items-center gap-2 border px-3 font-medium',
'justify-center p-0 sm:w-auto sm:justify-start sm:px-3',
'w-7 sm:w-auto',
'w-8',
'bg-background text-foreground',
'hover:bg-accent transition-colors',
'focus:!ring-0 focus:!outline-none',
@@ -127,11 +128,12 @@ const GroupTriggerButton = React.forwardRef<
ref={ref}
variant='outline'
role='combobox'
size='sm'
disabled={isDisabled}
className={cn(
'flex items-center gap-2 border px-3 font-medium',
'flex h-8 items-center gap-2 border px-3 font-medium',
'justify-center p-0 sm:w-auto sm:justify-start sm:px-3',
'w-7 sm:w-auto',
'w-8',
'bg-background text-foreground',
'hover:bg-accent transition-colors',
'focus:!ring-0 focus:!outline-none',
@@ -228,7 +230,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = React.memo(
{!isMobile && (
<CommandInput
placeholder={t('Search models...')}
className=''
className='h-9'
value={searchQuery}
onValueChange={setSearchQuery}
/>
@@ -251,7 +253,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = React.memo(
<div
className={cn(
'text-muted-foreground px-2 py-1 font-medium',
isMobile ? 'text-xs' : 'text-xs'
isMobile ? 'text-xs' : 'text-[10px]'
)}
>
{t('{{category}} Models', { category })}
@@ -272,7 +274,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = React.memo(
<div
className={cn(
'truncate font-medium',
isMobile ? 'text-sm' : 'text-xs'
isMobile ? 'text-sm' : 'text-[11px]'
)}
>
<span className='inline'>{model.label}</span>
@@ -298,16 +300,14 @@ export const ModelSelector: React.FC<ModelSelectorProps> = React.memo(
return isMobile ? (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger
render={
<ModelTriggerButton
currentLabel={currentModel?.label || t('Model')}
triggerClassName={className}
isDisabled={disabled}
aria-expanded={open}
/>
}
/>
<DrawerTrigger asChild>
<ModelTriggerButton
currentLabel={currentModel?.label || t('Model')}
triggerClassName={className}
isDisabled={disabled}
aria-expanded={open}
/>
</DrawerTrigger>
<DrawerContent className='flex max-h-[80vh] min-h-[60vh] flex-col'>
<DrawerHeader className='flex-shrink-0 pb-4'>
<DrawerTitle className='flex items-center gap-2 text-left text-lg font-medium'>
@@ -336,6 +336,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = React.memo(
align='start'
side='bottom'
sideOffset={4}
collisionPadding={8}
>
{renderModelCommandContent()}
</PopoverContent>
@@ -393,13 +394,13 @@ export const GroupSelector: React.FC<GroupSelectorProps> = React.memo(
return searchableFields.includes(searchTerm) ? 1 : 0
}}
>
<CommandInput placeholder={t('Search groups...')} className='' />
<CommandInput placeholder={t('Search groups...')} className='h-9' />
<CommandEmpty>{t('No group found.')}</CommandEmpty>
<CommandList
className={isMobile ? '!max-h-full flex-1 p-2' : 'max-h-[240px]'}
>
<CommandGroup>
<div className='text-muted-foreground px-2 py-1 text-xs font-medium'>
<div className='text-muted-foreground px-2 py-1 text-[10px] font-medium'>
{t('Model Group')}
</div>
{groups.map((group) => (
@@ -416,7 +417,7 @@ export const GroupSelector: React.FC<GroupSelectorProps> = React.memo(
>
<div className='flex min-w-0 flex-1 items-center gap-2 pr-4'>
<div className='flex min-w-0 flex-1 flex-col'>
<span className='text-foreground truncate text-xs font-medium'>
<span className='text-foreground truncate text-[11px] font-medium'>
{group.label}
</span>
{(group.desc || group.description) && (
@@ -447,16 +448,14 @@ export const GroupSelector: React.FC<GroupSelectorProps> = React.memo(
return isMobile ? (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger
render={
<GroupTriggerButton
currentLabel={currentGroup?.label || t('Group')}
triggerClassName={className}
isDisabled={disabled}
aria-expanded={open}
/>
}
/>
<DrawerTrigger asChild>
<GroupTriggerButton
currentLabel={currentGroup?.label || t('Group')}
triggerClassName={className}
isDisabled={disabled}
aria-expanded={open}
/>
</DrawerTrigger>
<DrawerContent className='max-h-[80vh]'>
<DrawerHeader className='pb-4 text-left'>
<DrawerTitle>{t('Choose Group')}</DrawerTitle>
@@ -469,7 +468,7 @@ export const GroupSelector: React.FC<GroupSelectorProps> = React.memo(
variant='outline'
onClick={() => handleGroupChange(group.value)}
className={cn(
'flex h-auto sm:h-auto w-full items-center justify-between rounded-lg p-4 text-left whitespace-normal',
'flex h-auto w-full items-center justify-between rounded-lg p-4 text-left whitespace-normal',
'border-border hover:bg-accent',
selectedGroup === group.value
? 'bg-accent border-primary/20'
@@ -527,6 +526,7 @@ export const GroupSelector: React.FC<GroupSelectorProps> = React.memo(
align='start'
side='bottom'
sideOffset={4}
collisionPadding={8}
>
{renderGroupCommandContent()}
</PopoverContent>
@@ -644,20 +644,21 @@ export const ModelGroupSelector: React.FC<ModelGroupSelectorProps> = ({
<Button
aria-expanded={open}
className={cn(
'max-w-[15rem] justify-start gap-2 border px-2.5 font-medium shadow-none',
'h-8 max-w-[15rem] justify-start gap-2 border px-2.5 font-medium shadow-none',
'bg-background/80 hover:bg-accent/70 text-foreground',
'focus:!ring-0 focus:!outline-none',
className
)}
disabled={disabled}
role='combobox'
size='sm'
variant='outline'
>
<CpuIcon className='text-muted-foreground size-4 shrink-0' />
<span className='min-w-0 truncate text-xs'>
{currentModel?.label || t('Model')}
</span>
<span className='bg-muted text-muted-foreground hidden max-w-20 shrink-0 rounded px-1.5 py-0.5 text-xs sm:inline-flex'>
<span className='bg-muted text-muted-foreground hidden max-w-20 shrink-0 rounded px-1.5 py-0.5 text-[10px] sm:inline-flex'>
{currentGroup?.label || t('Group')}
</span>
<ChevronsUpDown className='text-muted-foreground ml-auto size-3.5 shrink-0 opacity-60' />
@@ -671,7 +672,7 @@ export const ModelGroupSelector: React.FC<ModelGroupSelectorProps> = ({
!isMobile && modelGroupSelectorLayoutClasses.groupColumn
)}
>
<div className='text-muted-foreground px-1 text-xs leading-4 font-medium'>
<div className='text-muted-foreground px-1 text-[11px] leading-4 font-medium'>
{t('Model Group')}
</div>
<div
@@ -724,7 +725,7 @@ export const ModelGroupSelector: React.FC<ModelGroupSelectorProps> = ({
shouldFilter={false}
>
<CommandInput
className='text-[13px]'
className='h-8 text-[13px]'
onValueChange={setSearchQuery}
placeholder={t('Search models...')}
value={searchQuery}
@@ -803,7 +804,7 @@ export const ModelGroupSelector: React.FC<ModelGroupSelectorProps> = ({
return isMobile ? (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger render={renderTrigger()} />
<DrawerTrigger asChild>{renderTrigger()}</DrawerTrigger>
<DrawerContent className='flex max-h-[80vh] min-h-[60vh] flex-col'>
<DrawerHeader className='pb-3 text-left'>
<DrawerTitle>{t('Select Model')}</DrawerTitle>
@@ -822,6 +823,7 @@ export const ModelGroupSelector: React.FC<ModelGroupSelectorProps> = ({
'bg-popover z-50 w-[34rem] max-w-[calc(100vw-2rem)] rounded-xl border p-0 shadow-lg',
modelGroupSelectorLayoutClasses.desktopPanel
)}
collisionPadding={8}
side='top'
sideOffset={8}
>
+1 -1
View File
@@ -34,7 +34,7 @@ import {
ComboboxList,
ComboboxValue,
useComboboxAnchor,
} from '@/components/design-system/combobox'
} from '@/components/ui/combobox'
import { copyToClipboard } from '@/lib/copy-to-clipboard'
import { cn } from '@/lib/utils'
+8 -1
View File
@@ -32,5 +32,12 @@ export function NavigationProgress() {
}
}, [state.status])
return <LoadingBar color='var(--primary)' ref={ref} shadow height={2} />
return (
<LoadingBar
color='var(--muted-foreground)'
ref={ref}
shadow={true}
height={2}
/>
)
}
+7 -10
View File
@@ -20,15 +20,9 @@ import type { TFunction } from 'i18next'
import { Bell, Megaphone } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/design-system/tabs'
import { RichContent } from '@/components/rich-content'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Empty,
EmptyDescription,
@@ -45,6 +39,7 @@ import {
} from '@/components/ui/popover'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { getAnnouncementColorClass } from '@/lib/colors'
import { formatDateTimeObject } from '@/lib/time'
import { cn } from '@/lib/utils'
@@ -314,7 +309,7 @@ export function NotificationPopover({
<Button
variant='ghost'
size='icon'
className={cn('relative', className)}
className={cn('relative size-9', className)}
aria-label={t('Notifications')}
/>
}
@@ -323,7 +318,7 @@ export function NotificationPopover({
{unreadCount > 0 ? (
<Badge
variant='destructive'
className='absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center px-1 text-xs font-semibold tabular-nums'
className='absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center px-1 text-[10px] font-semibold tabular-nums'
>
{unreadCount > 99 ? '99+' : unreadCount}
</Badge>
@@ -371,7 +366,9 @@ export function NotificationPopover({
</Tabs>
<div className='flex justify-end'>
<Button onClick={() => onOpenChange(false)}>{t('Close')}</Button>
<Button size='sm' onClick={() => onOpenChange(false)}>
{t('Close')}
</Button>
</div>
</PopoverContent>
</Popover>
+76 -12
View File
@@ -20,7 +20,16 @@ import { Outlet, useRouterState } from '@tanstack/react-router'
import { motion, useReducedMotion, type Variants } from 'motion/react'
import type { ReactNode } from 'react'
import { MOTION_TRANSITION, MOTION_VARIANTS } from '@/lib/motion'
import {
CARD_ITEM_VARIANTS,
CARD_STAGGER_VARIANTS,
MOTION_TRANSITION,
MOTION_VARIANTS,
STAGGER_ITEM_VARIANTS,
STAGGER_VARIANTS,
TABLE_ROW_VARIANTS,
TABLE_STAGGER_VARIANTS,
} from '@/lib/motion'
interface PageTransitionProps {
children: ReactNode
@@ -83,13 +92,23 @@ interface StaggerContainerProps {
variants?: Variants
}
/* Entrance choreography is retired on admin surfaces: content appears
* immediately so tables and dashboards read as instant. The components stay
* as plain wrappers so call sites keep working; decorative motion remains
* available to the landing page via its dedicated landing-* utilities. */
export function StaggerContainer(props: StaggerContainerProps) {
return <div className={props.className}>{props.children}</div>
const shouldReduce = useReducedMotion()
if (shouldReduce) {
return <div className={props.className}>{props.children}</div>
}
return (
<motion.div
variants={props.variants ?? STAGGER_VARIANTS}
initial='initial'
animate='animate'
className={props.className}
>
{props.children}
</motion.div>
)
}
interface StaggerItemProps {
@@ -99,23 +118,68 @@ interface StaggerItemProps {
}
export function StaggerItem(props: StaggerItemProps) {
return <div className={props.className}>{props.children}</div>
return (
<motion.div
variants={props.variants ?? STAGGER_ITEM_VARIANTS}
className={props.className}
>
{props.children}
</motion.div>
)
}
export function TableStaggerContainer(props: StaggerContainerProps) {
return <tbody className={props.className}>{props.children}</tbody>
const shouldReduce = useReducedMotion()
if (shouldReduce) {
return <>{props.children}</>
}
return (
<motion.tbody
variants={TABLE_STAGGER_VARIANTS}
initial='initial'
animate='animate'
className={props.className}
>
{props.children}
</motion.tbody>
)
}
export function TableStaggerRow(props: StaggerItemProps) {
return <tr className={props.className}>{props.children}</tr>
return (
<motion.tr variants={TABLE_ROW_VARIANTS} className={props.className}>
{props.children}
</motion.tr>
)
}
export function CardStaggerContainer(props: StaggerContainerProps) {
return <div className={props.className}>{props.children}</div>
const shouldReduce = useReducedMotion()
if (shouldReduce) {
return <div className={props.className}>{props.children}</div>
}
return (
<motion.div
variants={CARD_STAGGER_VARIANTS}
initial='initial'
animate='animate'
className={props.className}
>
{props.children}
</motion.div>
)
}
export function CardStaggerItem(props: StaggerItemProps) {
return <div className={props.className}>{props.children}</div>
return (
<motion.div variants={CARD_ITEM_VARIANTS} className={props.className}>
{props.children}
</motion.div>
)
}
interface FadeInProps {
+5 -4
View File
@@ -19,10 +19,11 @@ For commercial licensing, please contact support@quantumnous.com
import { Eye, EyeOff } from 'lucide-react'
import * as React from 'react'
import { Button } from '@/components/design-system/button'
import { Input } from '@/components/design-system/input'
import { cn } from '@/lib/utils'
import { Button } from './ui/button'
import { Input } from './ui/input'
type PasswordInputProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'type'
@@ -48,10 +49,10 @@ export function PasswordInput({
/>
<Button
type='button'
size='icon-xs'
size='icon'
variant='ghost'
disabled={disabled}
className='text-muted-foreground absolute end-1 top-1/2 -translate-y-1/2'
className='text-muted-foreground absolute end-1 top-1/2 h-6 w-6 -translate-y-1/2 rounded-md'
onClick={() => setShowPassword((prev) => !prev)}
aria-label='Toggle password visibility'
>

Some files were not shown because too many files have changed in this diff Show More