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:
t0ng7u
2026-07-12 18:28:07 +08:00
parent ed134bbd2b
commit 384928c49a
22 changed files with 287 additions and 83 deletions
+95 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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>
@@ -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>
)}
@@ -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
@@ -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
)}
>
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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": "チャネルタイプを選択",
+2
View File
@@ -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": "Выбрать тип канала",
+2
View File
@@ -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
View File
@@ -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": "選擇渠道類型",
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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