fix(model-pricing): polish sync channel dialog layout

- keep input focus and invalid rings inside the shared Input component to avoid clipping in constrained containers.
- make the sync channel selector table use a fixed header, internal body scrolling, and fixed pagination inside the dialog.
- align status cells with their header and translate channel status labels through existing i18n keys.
This commit is contained in:
QuentinHsu
2026-07-10 23:56:18 +08:00
parent 489c045842
commit 43783286e5
3 changed files with 41 additions and 19 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
type={type}
data-slot='input'
className={cn(
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 focus-visible:ring-inset disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 aria-invalid:ring-inset md:text-sm',
className
)}
{...props}
@@ -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 { type ColumnDef, type RowSelectionState } from '@tanstack/react-table'
import type { ColumnDef, RowSelectionState } from '@tanstack/react-table'
import { Search } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -119,6 +119,8 @@ export function ChannelSelectorDialog({
() => [
{
id: 'select',
size: 44,
minSize: 44,
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
@@ -142,6 +144,8 @@ export function ChannelSelectorDialog({
{
accessorKey: 'name',
header: t('Name'),
size: 300,
minSize: 220,
cell: ({ row }) => {
const name = row.getValue('name') as string
const channel = row.original
@@ -165,6 +169,8 @@ export function ChannelSelectorDialog({
{
accessorKey: 'base_url',
header: t('Base URL'),
size: 340,
minSize: 260,
cell: ({ row }) => {
const url = row.getValue('base_url') as string
return (
@@ -180,6 +186,8 @@ export function ChannelSelectorDialog({
{
accessorKey: 'status',
header: t('Status'),
size: 140,
minSize: 120,
cell: ({ row }) => {
const status = row.getValue('status') as number
const config =
@@ -198,7 +206,7 @@ export function ChannelSelectorDialog({
return (
<StatusBadge
label={config.label}
label={t(config.label)}
variant={config.variant}
size='sm'
copyable={false}
@@ -209,6 +217,8 @@ export function ChannelSelectorDialog({
{
id: 'endpoint',
header: t('Sync Endpoint'),
size: 460,
minSize: 360,
cell: ({ row }) => {
const channel = row.original
const currentEndpoint =
@@ -224,14 +234,12 @@ export function ChannelSelectorDialog({
}
return (
<div className='flex items-center gap-2'>
<div className='flex min-w-0 items-center gap-2'>
<Select
items={[
...ENDPOINT_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
})),
]}
items={ENDPOINT_OPTIONS.map((option) => ({
value: option.value,
label: option.label,
}))}
value={endpointType}
onValueChange={(v) => v !== null && handleTypeChange(v)}
>
@@ -253,7 +261,7 @@ export function ChannelSelectorDialog({
value={currentEndpoint}
onChange={(e) => updateEndpoint(channel.id, e.target.value)}
placeholder={t('/your/endpoint')}
className='h-8 w-40 font-mono text-xs'
className='h-8 min-w-0 flex-1 font-mono text-xs'
/>
)}
</div>
@@ -315,7 +323,7 @@ export function ChannelSelectorDialog({
)}
contentClassName='flex max-h-[90vh] max-w-[calc(100%-2rem)] flex-col sm:max-w-[90vw] xl:max-w-[1400px]'
contentHeight='min(72vh, 720px)'
bodyClassName='space-y-4'
bodyClassName='flex h-full min-h-0 flex-col overflow-hidden'
footer={
<>
<Button variant='outline' onClick={() => onOpenChange(false)}>
@@ -325,27 +333,41 @@ export function ChannelSelectorDialog({
</>
}
>
<div className='flex flex-1 flex-col gap-4 overflow-hidden'>
<div className='flex items-center gap-2'>
<div className='flex h-full min-h-0 flex-col gap-4 overflow-hidden'>
<div className='flex shrink-0 items-center gap-2'>
<div className='relative flex-1'>
<Search className='text-muted-foreground absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2' />
<Search className='text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2' />
<Input
placeholder={t('Search by name or URL...')}
value={search}
onChange={(e) => setSearch(e.target.value)}
className='ps-8'
className='ps-9'
/>
</div>
</div>
<DataTableView
table={table}
containerClassName='flex-1 overflow-auto rounded-md'
containerClassName='min-h-0 flex-1 rounded-md'
tableContainerClassName='h-full min-h-0'
tableHeaderClassName='[background-color:var(--table-header)]'
splitHeaderScrollClassName='h-full'
bodyContainerClassName='[scrollbar-gutter:stable]'
splitHeader
getColumnClassName={(columnId, part) => {
if (columnId === 'select') return 'w-11 text-center align-middle'
if (columnId === 'status') {
return part === 'header' ? 'h-11 align-middle' : 'align-middle'
}
return part === 'header' ? 'h-11 align-middle' : 'align-middle'
}}
emptyContent={t('No channels found')}
emptyCellClassName='h-24 text-center'
/>
<DataTablePagination table={table} />
<div className='shrink-0'>
<DataTablePagination table={table} />
</div>
</div>
</Dialog>
)
@@ -73,5 +73,5 @@ export const RATIO_TYPE_OPTIONS = [
export const CHANNEL_STATUS_CONFIG = {
1: { label: 'Enabled', variant: 'success' as const },
2: { label: 'Disabled', variant: 'danger' as const },
3: { label: 'Auto-Disabled', variant: 'warning' as const },
3: { label: 'Auto Disabled', variant: 'warning' as const },
} as const