mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-03 04:43:01 +00:00
feat(web): add badge size theme axis and polish mobile log cards and skeletons
- New data-theme-badge axis (default/lg/xl, site default lg) with config drawer section, cookie persistence, and unlayered CSS overrides; unpin h-5! on model badge so the setting takes effect - Right-align and unify type ramp of compound cells inside mobile log card rows via in-data-[slot=data-table-card-value] variants - Fix skeleton overflow on narrow screens at call sites (min-w-0, shrink-0, max-w-full) and mirror real toolbar/stat layouts - Show theme settings trigger on mobile header
This commit is contained in:
+95
-8
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { Radio as RadioPrimitive } from '@base-ui/react/radio'
|
||||
import { RadioGroup as Radio } from '@base-ui/react/radio-group'
|
||||
import { CircleCheck, Palette, RotateCcw } from 'lucide-react'
|
||||
import { type SVGProps } from 'react'
|
||||
import type { SVGProps } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { IconDir } from '@/assets/custom/icon-dir'
|
||||
@@ -56,6 +56,7 @@ import { useTheme } from '@/context/theme-provider'
|
||||
import {
|
||||
type ContentLayout,
|
||||
THEME_PRESETS,
|
||||
type ThemeBadgeSize,
|
||||
type ThemeFont,
|
||||
type ThemePreset,
|
||||
type ThemeRadius,
|
||||
@@ -121,6 +122,7 @@ export function ConfigDrawer({
|
||||
<FontConfig />
|
||||
<RadiusConfig />
|
||||
<ScaleConfig />
|
||||
<BadgeSizeConfig />
|
||||
{showLayoutControls && (
|
||||
<>
|
||||
<SidebarConfig />
|
||||
@@ -517,13 +519,15 @@ function ScalePreview(props: { rows: number; rowGap: string }) {
|
||||
className='absolute inset-2.5 flex flex-col justify-center'
|
||||
style={{ gap: props.rowGap }}
|
||||
>
|
||||
{Array.from({ length: props.rows }).map((_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className='bg-foreground/60 block h-[2px] rounded-full'
|
||||
style={{ width: `${85 - i * 10}%` }}
|
||||
/>
|
||||
))}
|
||||
{Array.from({ length: props.rows }, (_, i) => `${85 - i * 10}%`).map(
|
||||
(width) => (
|
||||
<span
|
||||
key={width}
|
||||
className='bg-foreground/60 block h-[2px] rounded-full'
|
||||
style={{ width }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -589,6 +593,89 @@ function ScaleConfig() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock pill rendered inside the badge-size preview tiles. Each option shows
|
||||
* the pill at the proportions that size will actually produce.
|
||||
*/
|
||||
function BadgeSizePreview(props: { height: string; fontSize: string }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden='true'
|
||||
className='absolute inset-0 flex items-center justify-center'
|
||||
>
|
||||
<span
|
||||
className='border-foreground/50 text-foreground/70 inline-flex items-center rounded-full border px-2 leading-none font-medium'
|
||||
style={{ height: props.height, fontSize: props.fontSize }}
|
||||
>
|
||||
Abc
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BadgeSizeConfig() {
|
||||
const { t } = useTranslation()
|
||||
const { defaults, customization, setBadgeSize } = useThemeCustomization()
|
||||
const badgeSizeOptions: {
|
||||
value: ThemeBadgeSize
|
||||
label: string
|
||||
height: string
|
||||
fontSize: string
|
||||
}[] = [
|
||||
{ value: 'default', label: t('Compact'), height: '20px', fontSize: '11px' },
|
||||
{ value: 'lg', label: t('Comfortable'), height: '24px', fontSize: '12px' },
|
||||
{ value: 'xl', label: t('Super Large'), height: '28px', fontSize: '13px' },
|
||||
]
|
||||
return (
|
||||
<div>
|
||||
<SectionTitle
|
||||
title={t('Badge size')}
|
||||
showReset={customization.badgeSize !== defaults.badgeSize}
|
||||
onReset={() => setBadgeSize(defaults.badgeSize)}
|
||||
/>
|
||||
<Radio
|
||||
value={customization.badgeSize}
|
||||
onValueChange={(v) => setBadgeSize(v as ThemeBadgeSize)}
|
||||
className='grid w-full grid-cols-3 gap-3'
|
||||
aria-label={t('Select badge size')}
|
||||
>
|
||||
{badgeSizeOptions.map((option) => (
|
||||
<Item
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className='group flex flex-col items-stretch outline-none'
|
||||
aria-label={option.label}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'ring-border relative h-12 rounded-md ring-[1px] transition',
|
||||
'group-data-checked:ring-primary group-data-checked:shadow-md',
|
||||
'group-focus-visible:ring-2',
|
||||
'group-hover:ring-primary/60'
|
||||
)}
|
||||
>
|
||||
<CircleCheck
|
||||
className={cn(
|
||||
'fill-primary absolute top-0 right-0 z-10 size-5 translate-x-1/2 -translate-y-1/2 stroke-white',
|
||||
'group-data-unchecked:hidden'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<BadgeSizePreview
|
||||
height={option.height}
|
||||
fontSize={option.fontSize}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-1.5 truncate text-center text-xs'>
|
||||
{option.label}
|
||||
</div>
|
||||
</Item>
|
||||
))}
|
||||
</Radio>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarConfig() {
|
||||
const { t } = useTranslation()
|
||||
const { defaultVariant, variant, setVariant } = useLayout()
|
||||
|
||||
+34
-34
@@ -25,7 +25,7 @@ import { useNotifications } from '@/hooks/use-notifications'
|
||||
import { useTopNavLinks } from '@/hooks/use-top-nav-links'
|
||||
|
||||
import { defaultTopNavLinks } from '../config/top-nav.config'
|
||||
import { type TopNavLink } from '../types'
|
||||
import type { TopNavLink } from '../types'
|
||||
import { Header } from './header'
|
||||
import { SystemBrand } from './system-brand'
|
||||
import { TopNav } from './top-nav'
|
||||
@@ -111,40 +111,40 @@ export function AppHeader({
|
||||
const notifications = useNotifications()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<SystemBrand variant='inline' />
|
||||
<Header>
|
||||
<SystemBrand variant='inline' />
|
||||
|
||||
{leftContent ? (
|
||||
<div className='ms-2 flex items-center'>{leftContent}</div>
|
||||
) : null}
|
||||
{leftContent ? (
|
||||
<div className='ms-2 flex items-center'>{leftContent}</div>
|
||||
) : null}
|
||||
|
||||
{rightContent ?? (
|
||||
<div className='ms-auto flex items-center gap-1 sm:gap-2'>
|
||||
{showTopNav && (
|
||||
<div className='me-1 hidden lg:block'>
|
||||
<TopNav links={links} />
|
||||
</div>
|
||||
)}
|
||||
{showSearch && <Search />}
|
||||
{showNotifications && (
|
||||
<NotificationPopover
|
||||
open={notifications.popoverOpen}
|
||||
onOpenChange={notifications.setPopoverOpen}
|
||||
unreadCount={notifications.unreadCount}
|
||||
activeTab={notifications.activeTab}
|
||||
onTabChange={notifications.setActiveTab}
|
||||
notice={notifications.notice}
|
||||
announcements={notifications.announcements}
|
||||
loading={notifications.loading}
|
||||
/>
|
||||
)}
|
||||
<LanguageSwitcher />
|
||||
{showConfigDrawer && <ConfigDrawer />}
|
||||
{showProfileDropdown && <ProfileDropdown />}
|
||||
</div>
|
||||
)}
|
||||
</Header>
|
||||
</>
|
||||
{rightContent ?? (
|
||||
<div className='ms-auto flex items-center gap-1 sm:gap-2'>
|
||||
{showTopNav && (
|
||||
<div className='me-1 hidden lg:block'>
|
||||
<TopNav links={links} />
|
||||
</div>
|
||||
)}
|
||||
{showSearch && <Search />}
|
||||
{showNotifications && (
|
||||
<NotificationPopover
|
||||
open={notifications.popoverOpen}
|
||||
onOpenChange={notifications.setPopoverOpen}
|
||||
unreadCount={notifications.unreadCount}
|
||||
activeTab={notifications.activeTab}
|
||||
onTabChange={notifications.setActiveTab}
|
||||
notice={notifications.notice}
|
||||
announcements={notifications.announcements}
|
||||
loading={notifications.loading}
|
||||
/>
|
||||
)}
|
||||
<LanguageSwitcher />
|
||||
{showConfigDrawer && (
|
||||
<ConfigDrawer triggerClassName='max-md:inline-flex' />
|
||||
)}
|
||||
{showProfileDropdown && <ProfileDropdown />}
|
||||
</div>
|
||||
)}
|
||||
</Header>
|
||||
)
|
||||
}
|
||||
|
||||
+37
-2
@@ -31,11 +31,13 @@ import {
|
||||
type ContentLayout,
|
||||
DEFAULT_THEME_CUSTOMIZATION,
|
||||
resolveThemeFont,
|
||||
THEME_BADGE_SIZE_VALUES,
|
||||
THEME_COOKIE_KEYS,
|
||||
THEME_FONT_VALUES,
|
||||
THEME_PRESET_VALUES,
|
||||
THEME_RADIUS_VALUES,
|
||||
THEME_SCALE_VALUES,
|
||||
type ThemeBadgeSize,
|
||||
type ThemeCustomization,
|
||||
type ThemeFont,
|
||||
type ThemePreset,
|
||||
@@ -72,6 +74,7 @@ type ThemeCustomizationContextType = {
|
||||
setFont: (font: ThemeFont) => void
|
||||
setRadius: (radius: ThemeRadius) => void
|
||||
setScale: (scale: ThemeScale) => void
|
||||
setBadgeSize: (badgeSize: ThemeBadgeSize) => void
|
||||
setContentLayout: (contentLayout: ContentLayout) => void
|
||||
resetCustomization: () => void
|
||||
}
|
||||
@@ -87,6 +90,7 @@ const FALLBACK_CONTEXT: ThemeCustomizationContextType = {
|
||||
setFont: () => {},
|
||||
setRadius: () => {},
|
||||
setScale: () => {},
|
||||
setBadgeSize: () => {},
|
||||
setContentLayout: () => {},
|
||||
resetCustomization: () => {},
|
||||
}
|
||||
@@ -125,6 +129,13 @@ export function ThemeCustomizationProvider(props: {
|
||||
DEFAULT_THEME_CUSTOMIZATION.scale
|
||||
)
|
||||
)
|
||||
const [badgeSize, _setBadgeSize] = useState<ThemeBadgeSize>(() =>
|
||||
readCookie<ThemeBadgeSize>(
|
||||
THEME_COOKIE_KEYS.badgeSize,
|
||||
THEME_BADGE_SIZE_VALUES,
|
||||
DEFAULT_THEME_CUSTOMIZATION.badgeSize
|
||||
)
|
||||
)
|
||||
const [contentLayout, _setContentLayout] = useState<ContentLayout>(() =>
|
||||
readCookie<ContentLayout>(
|
||||
THEME_COOKIE_KEYS.contentLayout,
|
||||
@@ -168,6 +179,17 @@ export function ThemeCustomizationProvider(props: {
|
||||
)
|
||||
}, [scale])
|
||||
|
||||
// Unlike the other axes, the *site default* for badge size is `lg`, which
|
||||
// is a styled value — so the attribute is keyed on the literal unstyled
|
||||
// `default` tier rather than DEFAULT_THEME_CUSTOMIZATION (removing the
|
||||
// attribute for `lg` would silently drop its CSS).
|
||||
useLayoutEffect(() => {
|
||||
applyAttribute(
|
||||
'data-theme-badge',
|
||||
badgeSize === 'default' ? null : badgeSize
|
||||
)
|
||||
}, [badgeSize])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
applyAttribute('data-theme-content-layout', contentLayout)
|
||||
}, [contentLayout])
|
||||
@@ -208,6 +230,15 @@ export function ThemeCustomizationProvider(props: {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setBadgeSize = useCallback((value: ThemeBadgeSize) => {
|
||||
_setBadgeSize(value)
|
||||
if (value === DEFAULT_THEME_CUSTOMIZATION.badgeSize) {
|
||||
removeCookie(THEME_COOKIE_KEYS.badgeSize)
|
||||
} else {
|
||||
setCookie(THEME_COOKIE_KEYS.badgeSize, value, COOKIE_MAX_AGE)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setContentLayout = useCallback((value: ContentLayout) => {
|
||||
_setContentLayout(value)
|
||||
if (value === DEFAULT_THEME_CUSTOMIZATION.contentLayout) {
|
||||
@@ -222,17 +253,19 @@ export function ThemeCustomizationProvider(props: {
|
||||
setFont(DEFAULT_THEME_CUSTOMIZATION.font)
|
||||
setRadius(DEFAULT_THEME_CUSTOMIZATION.radius)
|
||||
setScale(DEFAULT_THEME_CUSTOMIZATION.scale)
|
||||
setBadgeSize(DEFAULT_THEME_CUSTOMIZATION.badgeSize)
|
||||
setContentLayout(DEFAULT_THEME_CUSTOMIZATION.contentLayout)
|
||||
}, [setPreset, setFont, setRadius, setScale, setContentLayout])
|
||||
}, [setPreset, setFont, setRadius, setScale, setBadgeSize, setContentLayout])
|
||||
|
||||
const value = useMemo<ThemeCustomizationContextType>(
|
||||
() => ({
|
||||
defaults: DEFAULT_THEME_CUSTOMIZATION,
|
||||
customization: { preset, font, radius, scale, contentLayout },
|
||||
customization: { preset, font, radius, scale, badgeSize, contentLayout },
|
||||
setPreset,
|
||||
setFont,
|
||||
setRadius,
|
||||
setScale,
|
||||
setBadgeSize,
|
||||
setContentLayout,
|
||||
resetCustomization,
|
||||
}),
|
||||
@@ -241,11 +274,13 @@ export function ThemeCustomizationProvider(props: {
|
||||
font,
|
||||
radius,
|
||||
scale,
|
||||
badgeSize,
|
||||
contentLayout,
|
||||
setPreset,
|
||||
setFont,
|
||||
setRadius,
|
||||
setScale,
|
||||
setBadgeSize,
|
||||
setContentLayout,
|
||||
resetCustomization,
|
||||
]
|
||||
|
||||
@@ -81,8 +81,9 @@ function CardContentSkeleton() {
|
||||
function FilterBarSkeleton() {
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex flex-1 flex-wrap items-center gap-2'>
|
||||
{/* Mirrors PricingToolbar: stacked on mobile, single row from lg. */}
|
||||
<div className='flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between'>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
{[80, 90, 75, 85, 70].map((width) => (
|
||||
<Skeleton
|
||||
key={`chip-${width}`}
|
||||
@@ -91,7 +92,7 @@ function FilterBarSkeleton() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<Skeleton className='h-8 w-24 rounded-lg' />
|
||||
<Skeleton className='h-8 w-20 rounded-lg' />
|
||||
<Skeleton className='h-8 w-24' />
|
||||
|
||||
@@ -228,11 +228,13 @@ export function CheckinCalendarCard({
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<div className='p-4 sm:p-5'>
|
||||
<div className='flex items-start justify-between gap-4'>
|
||||
<div className='space-y-2'>
|
||||
<Skeleton className='h-5 w-32' />
|
||||
<Skeleton className='h-4 w-56' />
|
||||
{/* min-w-0 + max-w-full let the fixed-width lines clamp to the
|
||||
card on narrow screens instead of overflowing. */}
|
||||
<div className='min-w-0 space-y-2'>
|
||||
<Skeleton className='h-5 w-32 max-w-full' />
|
||||
<Skeleton className='h-4 w-56 max-w-full' />
|
||||
</div>
|
||||
<Skeleton className='h-8 w-28 rounded-md' />
|
||||
<Skeleton className='h-8 w-28 shrink-0 rounded-md' />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -42,10 +42,12 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardContent className='p-4 sm:p-5'>
|
||||
<div className='flex items-center gap-3 sm:gap-4'>
|
||||
<Skeleton className='size-12 rounded-xl sm:size-14' />
|
||||
<div className='space-y-2'>
|
||||
<Skeleton className='h-6 w-48' />
|
||||
<Skeleton className='h-4 w-64' />
|
||||
<Skeleton className='size-12 shrink-0 rounded-xl sm:size-14' />
|
||||
{/* min-w-0 + max-w-full let the fixed-width lines clamp to the
|
||||
card on narrow screens instead of overflowing. */}
|
||||
<div className='min-w-0 space-y-2'>
|
||||
<Skeleton className='h-6 w-48 max-w-full' />
|
||||
<Skeleton className='h-4 w-64 max-w-full' />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -53,8 +55,8 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
<div className='divide-border/60 grid grid-cols-3 divide-x'>
|
||||
{['balance', 'usage', 'requests'].map((key) => (
|
||||
<div key={key} className='px-4 py-3 sm:px-5 sm:py-4'>
|
||||
<Skeleton className='h-4 w-24' />
|
||||
<Skeleton className='mt-2 h-7 w-28' />
|
||||
<Skeleton className='h-4 w-24 max-w-full' />
|
||||
<Skeleton className='mt-2 h-7 w-28 max-w-full' />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+9
-7
@@ -294,8 +294,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
const config = getLogTypeConfig(log.type)
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-col gap-0.5'>
|
||||
<span className='text-xs tabular-nums'>
|
||||
// In mobile cards this cell sits in a label-left/value-right row
|
||||
// (data-table-card-value); align both lines to the right edge there.
|
||||
<div className='flex min-w-0 flex-col gap-0.5 in-data-[slot=data-table-card-value]:items-end'>
|
||||
<span className='text-xs tabular-nums in-data-[slot=data-table-card-value]:text-sm'>
|
||||
{formatTimestampToDate(timestamp)}
|
||||
</span>
|
||||
<StatusBadge variant={config.variant} size='sm'>
|
||||
@@ -369,7 +371,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div className='flex max-w-[160px] flex-col gap-0.5' />
|
||||
<div className='flex max-w-[160px] flex-col gap-0.5 in-data-[slot=data-table-card-value]:max-w-full in-data-[slot=data-table-card-value]:items-end' />
|
||||
}
|
||||
>
|
||||
<div className='relative inline-flex w-fit max-w-full items-center gap-1'>
|
||||
@@ -516,7 +518,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span className='text-muted-foreground max-w-[100px] truncate text-sm hover:underline' />
|
||||
<span className='text-muted-foreground max-w-[100px] truncate text-xs hover:underline in-data-[slot=data-table-card-value]:max-w-full in-data-[slot=data-table-card-value]:text-sm' />
|
||||
}
|
||||
>
|
||||
{sensitiveVisible ? log.username : '••••'}
|
||||
@@ -728,7 +730,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<span className='text-xs font-medium tabular-nums'>
|
||||
<span className='text-xs font-medium tabular-nums in-data-[slot=data-table-card-value]:text-sm'>
|
||||
{promptTokens.toLocaleString()} /{' '}
|
||||
{completionTokens.toLocaleString()}
|
||||
</span>
|
||||
@@ -851,13 +853,13 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
<>
|
||||
<button
|
||||
type='button'
|
||||
className='group flex max-w-[200px] flex-col gap-0.5 text-left text-sm'
|
||||
className='group flex max-w-[200px] flex-col gap-0.5 text-left text-xs in-data-[slot=data-table-card-value]:max-w-full in-data-[slot=data-table-card-value]:text-sm'
|
||||
onClick={() => setDialogOpen(true)}
|
||||
title={t('Click to view full details')}
|
||||
>
|
||||
{detailsContent}
|
||||
{ip && (
|
||||
<span className='text-subtle-foreground max-w-full truncate tabular-nums'>
|
||||
<span className='text-subtle-foreground max-w-full truncate text-xs tabular-nums'>
|
||||
{ip}
|
||||
</span>
|
||||
)}
|
||||
|
||||
+4
-2
@@ -94,8 +94,10 @@ export function useDrawingLogsColumns(
|
||||
const submitTime = row.getValue('submit_time') as number
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-col gap-0.5'>
|
||||
<span className='text-xs tabular-nums'>
|
||||
// In mobile cards this cell sits in a label-left/value-right row
|
||||
// (data-table-card-value); align both lines to the right edge there.
|
||||
<div className='flex min-w-0 flex-col gap-0.5 in-data-[slot=data-table-card-value]:items-end'>
|
||||
<span className='text-xs tabular-nums in-data-[slot=data-table-card-value]:text-sm'>
|
||||
{formatTimestampToDate(submitTime, 'milliseconds')}
|
||||
</span>
|
||||
<StatusBadge
|
||||
|
||||
+5
-3
@@ -52,8 +52,10 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
const submitTime = row.getValue('submit_time') as number
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-col gap-0.5'>
|
||||
<span className='text-xs tabular-nums'>
|
||||
// In mobile cards this cell sits in a label-left/value-right row
|
||||
// (data-table-card-value); align both lines to the right edge there.
|
||||
<div className='flex min-w-0 flex-col gap-0.5 in-data-[slot=data-table-card-value]:items-end'>
|
||||
<span className='text-xs tabular-nums in-data-[slot=data-table-card-value]:text-sm'>
|
||||
{formatTimestampToDate(submitTime, 'seconds')}
|
||||
</span>
|
||||
{log.finish_time ? (
|
||||
@@ -108,7 +110,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span
|
||||
className='text-muted-foreground max-w-[100px] truncate text-sm hover:underline'
|
||||
className='text-muted-foreground max-w-[100px] truncate text-xs hover:underline in-data-[slot=data-table-card-value]:max-w-full in-data-[slot=data-table-card-value]:text-sm'
|
||||
title={sensitiveVisible ? displayName : undefined}
|
||||
>
|
||||
{sensitiveVisible ? displayName : '••••'}
|
||||
|
||||
@@ -263,11 +263,6 @@ export function CommonLogsFilterBar<TData>(
|
||||
const logTypeLabel =
|
||||
logTypeItems.find((type) => type.value === logType)?.label ?? t('All Types')
|
||||
|
||||
const statsBar = (
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<CommonLogsStats />
|
||||
</div>
|
||||
)
|
||||
const sensitiveToggle = (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
@@ -412,7 +407,7 @@ export function CommonLogsFilterBar<TData>(
|
||||
return (
|
||||
<LogsFilterToolbar
|
||||
table={props.table}
|
||||
stats={statsBar}
|
||||
stats={<CommonLogsStats />}
|
||||
actionStart={sensitiveToggle}
|
||||
primaryFilters={
|
||||
<>
|
||||
|
||||
@@ -81,12 +81,15 @@ export function CommonLogsStats() {
|
||||
placeholderData: (previousData) => previousData,
|
||||
})
|
||||
|
||||
// Mirrors the loaded stat badges below: same wrapping row, and pill widths
|
||||
// matching the typical rendered size of "Usage ¥…", "RPM n", "TPM n" so the
|
||||
// row stays on one line on phones (~304px incl. gaps) without layout shift.
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Skeleton className='h-7 w-[150px] rounded-md' />
|
||||
<Skeleton className='h-7 w-[100px] rounded-md' />
|
||||
<Skeleton className='h-7 w-[120px] rounded-md' />
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<Skeleton className='h-6 w-28 rounded-full' />
|
||||
<Skeleton className='h-6 w-20 rounded-full' />
|
||||
<Skeleton className='h-6 w-24 rounded-full' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -164,7 +164,10 @@ function ModelBadgeContent(
|
||||
appearance='soft'
|
||||
size='sm'
|
||||
className={cn(
|
||||
'h-5! max-w-none shrink-0 whitespace-nowrap! [&_[data-slot=status-badge-label]]:whitespace-nowrap!',
|
||||
// Height comes from the badge size tier (and the optional
|
||||
// `data-theme-badge` axis); pinning it here with `h-5!` would
|
||||
// block the user-facing badge size setting.
|
||||
'max-w-none shrink-0 whitespace-nowrap! [&_[data-slot=status-badge-label]]:whitespace-nowrap!',
|
||||
colorClassName,
|
||||
props.className
|
||||
)}
|
||||
@@ -181,7 +184,7 @@ function ModelBadgeContent(
|
||||
appearance='soft'
|
||||
size='sm'
|
||||
className={cn(
|
||||
'h-5! max-w-none shrink-0 whitespace-nowrap! [&_[data-slot=status-badge-label]]:whitespace-nowrap!',
|
||||
'max-w-none shrink-0 whitespace-nowrap! [&_[data-slot=status-badge-label]]:whitespace-nowrap!',
|
||||
colorClassName,
|
||||
props.className
|
||||
)}
|
||||
|
||||
@@ -74,7 +74,10 @@ export function TimingMetricsCell(props: TimingMetricsCellProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-w-0 flex-col justify-center gap-0.5 text-xs leading-tight',
|
||||
// Mobile cards render this cell in a label-left/value-right row
|
||||
// (data-table-card-value): lines hug the right edge and use the
|
||||
// card's standard value size instead of the dense table size.
|
||||
'flex min-w-0 flex-col justify-center gap-0.5 text-xs leading-tight in-data-[slot=data-table-card-value]:items-end in-data-[slot=data-table-card-value]:text-sm',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
@@ -124,7 +127,7 @@ export function StreamTpsCell(props: StreamTpsCellProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex shrink-0 flex-col items-start justify-center gap-0.5 text-xs leading-tight',
|
||||
'flex shrink-0 flex-col items-start justify-center gap-0.5 text-xs leading-tight in-data-[slot=data-table-card-value]:items-end',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
|
||||
Vendored
+2
@@ -554,6 +554,7 @@
|
||||
"Backup codes remaining: {{count}}": "Backup codes remaining: {{count}}",
|
||||
"Bad Request": "Bad Request",
|
||||
"Badge Color": "Badge Color",
|
||||
"Badge size": "Badge size",
|
||||
"Baidu": "Baidu",
|
||||
"Baidu V2": "Baidu V2",
|
||||
"Balance": "Balance",
|
||||
@@ -4031,6 +4032,7 @@
|
||||
"Select announcement type": "Select announcement type",
|
||||
"Select at least one field to overwrite.": "Select at least one field to overwrite.",
|
||||
"Select at least one target model": "Select at least one target model",
|
||||
"Select badge size": "Select badge size",
|
||||
"Select body font": "Select body font",
|
||||
"Select border radius": "Select border radius",
|
||||
"Select channel type": "Select channel type",
|
||||
|
||||
Vendored
+2
@@ -554,6 +554,7 @@
|
||||
"Backup codes remaining: {{count}}": "Codes de secours restants : {{count}}",
|
||||
"Bad Request": "Requête invalide",
|
||||
"Badge Color": "Couleur du badge",
|
||||
"Badge size": "Taille des badges",
|
||||
"Baidu": "Baidu",
|
||||
"Baidu V2": "Baidu V2",
|
||||
"Balance": "Solde",
|
||||
@@ -4031,6 +4032,7 @@
|
||||
"Select announcement type": "Sélectionner le type d'annonce",
|
||||
"Select at least one field to overwrite.": "Sélectionnez au moins un champ à écraser.",
|
||||
"Select at least one target model": "Sélectionnez au moins un modèle cible",
|
||||
"Select badge size": "Sélectionner la taille des badges",
|
||||
"Select body font": "Sélectionner la police du corps de texte",
|
||||
"Select border radius": "Sélectionner le rayon de bordure",
|
||||
"Select channel type": "Sélectionner le type de canal",
|
||||
|
||||
Vendored
+2
@@ -554,6 +554,7 @@
|
||||
"Backup codes remaining: {{count}}": "残りのバックアップコード: {{count}}",
|
||||
"Bad Request": "不正リクエスト",
|
||||
"Badge Color": "バッジの色",
|
||||
"Badge size": "バッジサイズ",
|
||||
"Baidu": "Baidu",
|
||||
"Baidu V2": "Baidu V 2",
|
||||
"Balance": "残高",
|
||||
@@ -4031,6 +4032,7 @@
|
||||
"Select announcement type": "アナウンスメントタイプを選択",
|
||||
"Select at least one field to overwrite.": "上書きするフィールドを少なくとも 1 つ選択してください。",
|
||||
"Select at least one target model": "少なくとも1つの対象モデルを選択してください",
|
||||
"Select badge size": "バッジサイズを選択",
|
||||
"Select body font": "本文フォントを選択",
|
||||
"Select border radius": "角丸を選択",
|
||||
"Select channel type": "チャネルタイプを選択",
|
||||
|
||||
Vendored
+2
@@ -554,6 +554,7 @@
|
||||
"Backup codes remaining: {{count}}": "Осталось резервных кодов: {{count}}",
|
||||
"Bad Request": "Неверный запрос",
|
||||
"Badge Color": "Цвет значка",
|
||||
"Badge size": "Размер бейджей",
|
||||
"Baidu": "Baidu",
|
||||
"Baidu V2": "Baidu V2",
|
||||
"Balance": "Баланс",
|
||||
@@ -4031,6 +4032,7 @@
|
||||
"Select announcement type": "Выбрать тип объявления",
|
||||
"Select at least one field to overwrite.": "Выберите хотя бы одно поле для перезаписи.",
|
||||
"Select at least one target model": "Выберите хотя бы одну целевую модель",
|
||||
"Select badge size": "Выберите размер бейджей",
|
||||
"Select body font": "Выберите шрифт текста",
|
||||
"Select border radius": "Выберите радиус скругления",
|
||||
"Select channel type": "Выбрать тип канала",
|
||||
|
||||
Vendored
+2
@@ -554,6 +554,7 @@
|
||||
"Backup codes remaining: {{count}}": "Mã dự phòng còn lại: {{count}}",
|
||||
"Bad Request": "Yêu cầu không hợp lệ",
|
||||
"Badge Color": "Màu huy hiệu",
|
||||
"Badge size": "Kích thước huy hiệu",
|
||||
"Baidu": "Baidu",
|
||||
"Baidu V2": "Baidu V2",
|
||||
"Balance": "Cân bằng",
|
||||
@@ -4031,6 +4032,7 @@
|
||||
"Select announcement type": "Select notification type",
|
||||
"Select at least one field to overwrite.": "Chọn ít nhất một trường để ghi đè.",
|
||||
"Select at least one target model": "Chọn ít nhất một mô hình đích",
|
||||
"Select badge size": "Chọn kích thước huy hiệu",
|
||||
"Select body font": "Chọn phông chữ nội dung",
|
||||
"Select border radius": "Chọn độ bo góc",
|
||||
"Select channel type": "Chọn loại kênh",
|
||||
|
||||
+2
@@ -554,6 +554,7 @@
|
||||
"Backup codes remaining: {{count}}": "剩餘備用代碼:{{count}}",
|
||||
"Bad Request": "參數錯誤",
|
||||
"Badge Color": "徽章顏色",
|
||||
"Badge size": "徽章尺寸",
|
||||
"Baidu": "百度",
|
||||
"Baidu V2": "百度 V2",
|
||||
"Balance": "餘額",
|
||||
@@ -4031,6 +4032,7 @@
|
||||
"Select announcement type": "選擇公告類型",
|
||||
"Select at least one field to overwrite.": "請選擇至少一個要覆蓋的欄位。",
|
||||
"Select at least one target model": "請至少選擇一個目標模型",
|
||||
"Select badge size": "選擇徽章尺寸",
|
||||
"Select body font": "選擇正文字體",
|
||||
"Select border radius": "選擇圓角大小",
|
||||
"Select channel type": "選擇渠道類型",
|
||||
|
||||
Vendored
+2
@@ -554,6 +554,7 @@
|
||||
"Backup codes remaining: {{count}}": "剩余备份代码:{{count}}",
|
||||
"Bad Request": "参数错误",
|
||||
"Badge Color": "徽章颜色",
|
||||
"Badge size": "徽章尺寸",
|
||||
"Baidu": "百度",
|
||||
"Baidu V2": "百度 V2",
|
||||
"Balance": "余额",
|
||||
@@ -4031,6 +4032,7 @@
|
||||
"Select announcement type": "选择公告类型",
|
||||
"Select at least one field to overwrite.": "请选择至少一个要覆盖的字段。",
|
||||
"Select at least one target model": "请至少选择一个目标模型",
|
||||
"Select badge size": "选择徽章尺寸",
|
||||
"Select body font": "选择正文字体",
|
||||
"Select border radius": "选择圆角大小",
|
||||
"Select channel type": "选择渠道类型",
|
||||
|
||||
+17
@@ -82,6 +82,14 @@ export const THEME_PRESETS = [
|
||||
export type ThemePreset = (typeof THEME_PRESETS)[number]['value']
|
||||
export type ThemeRadius = 'default' | 'none' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
export type ThemeScale = 'default' | 'sm' | 'lg' | 'xl'
|
||||
|
||||
/**
|
||||
* Badge size axis. Controls how "chubby" badges/pills (Badge, StatusBadge)
|
||||
* read, independently of the global density scale. `default` is the compact
|
||||
* redesign look; `lg` restores the pre-redesign pill (larger text, full-pill
|
||||
* corners) and is the site default; `xl` is one step larger for readability.
|
||||
*/
|
||||
export type ThemeBadgeSize = 'default' | 'lg' | 'xl'
|
||||
export type ContentLayout = 'full' | 'centered'
|
||||
|
||||
/**
|
||||
@@ -111,6 +119,7 @@ export type ThemeCustomization = {
|
||||
font: ThemeFont
|
||||
radius: ThemeRadius
|
||||
scale: ThemeScale
|
||||
badgeSize: ThemeBadgeSize
|
||||
contentLayout: ContentLayout
|
||||
}
|
||||
|
||||
@@ -119,6 +128,7 @@ export const DEFAULT_THEME_CUSTOMIZATION: ThemeCustomization = {
|
||||
font: 'default',
|
||||
radius: 'default',
|
||||
scale: 'default',
|
||||
badgeSize: 'lg',
|
||||
contentLayout: 'full',
|
||||
}
|
||||
|
||||
@@ -149,6 +159,12 @@ export const THEME_SCALE_VALUES: ReadonlySet<ThemeScale> = new Set([
|
||||
'xl',
|
||||
])
|
||||
|
||||
export const THEME_BADGE_SIZE_VALUES: ReadonlySet<ThemeBadgeSize> = new Set([
|
||||
'default',
|
||||
'lg',
|
||||
'xl',
|
||||
])
|
||||
|
||||
export const CONTENT_LAYOUT_VALUES: ReadonlySet<ContentLayout> = new Set([
|
||||
'full',
|
||||
'centered',
|
||||
@@ -159,6 +175,7 @@ export const THEME_COOKIE_KEYS = {
|
||||
font: 'theme_font',
|
||||
radius: 'theme_radius',
|
||||
scale: 'theme_scale',
|
||||
badgeSize: 'theme_badge_size',
|
||||
contentLayout: 'theme_content_layout',
|
||||
} as const
|
||||
|
||||
|
||||
+36
@@ -728,6 +728,42 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
--spacing: 0.3rem;
|
||||
}
|
||||
|
||||
/* ── Badge size ───────────────────────────────────────────────────────── */
|
||||
/* Independent axis for how "chubby" badges/pills read (`data-theme-badge`
|
||||
* on <body>, driven by ThemeCustomizationProvider). The redesign moved
|
||||
* badges from 14px text + full-pill corners to 12px text + subtle rounding;
|
||||
* `lg` promotes every badge to the md tier (h-6 / text-sm) with pill
|
||||
* corners, `xl` goes one step larger for readability. Default keeps the
|
||||
* compact redesign look.
|
||||
*
|
||||
* These rules are intentionally unlayered so they win over the Tailwind
|
||||
* utility classes baked into the badge variants (unlayered author CSS
|
||||
* outranks `@layer utilities`). Text-only status badges
|
||||
* (`data-appearance='plain'`) only pick up the type ramp: forcing a fixed
|
||||
* height or padding on them would add phantom box space around bare text. */
|
||||
[data-theme-badge='lg'] [data-slot='badge'],
|
||||
[data-theme-badge='lg'] [data-slot='status-badge'] {
|
||||
font-size: var(--text-sm);
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
[data-theme-badge='lg'] [data-slot='badge'],
|
||||
[data-theme-badge='lg'] [data-slot='status-badge']:not([data-appearance='plain']) {
|
||||
height: calc(var(--spacing) * 6);
|
||||
gap: calc(var(--spacing) * 1.5);
|
||||
padding-inline: calc(var(--spacing) * 2);
|
||||
}
|
||||
[data-theme-badge='xl'] [data-slot='badge'],
|
||||
[data-theme-badge='xl'] [data-slot='status-badge'] {
|
||||
font-size: var(--text-base);
|
||||
border-radius: calc(infinity * 1px);
|
||||
}
|
||||
[data-theme-badge='xl'] [data-slot='badge'],
|
||||
[data-theme-badge='xl'] [data-slot='status-badge']:not([data-appearance='plain']) {
|
||||
height: calc(var(--spacing) * 7);
|
||||
gap: calc(var(--spacing) * 1.5);
|
||||
padding-inline: calc(var(--spacing) * 2.5);
|
||||
}
|
||||
|
||||
/* ── Content layout ───────────────────────────────────────────────────── */
|
||||
/* `centered` clamps inset content to a comfortable reading width on large screens.
|
||||
* Targets every direct child of <SidebarInset> so router outlet wrappers and
|
||||
|
||||
Reference in New Issue
Block a user