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:
+1
-1
@@ -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}
|
||||
|
||||
+39
-17
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user