style(web): unify design system across default frontend

🔤 Typography
- Fix --font-sans stack: 'Public Sans Variable' was never applied (wrong family name), add CJK fallbacks (PingFang SC / Microsoft YaHei UI / Noto Sans SC)
- Sync <html lang> with i18n language for correct CJK glyph selection
- Remove dead fake font system (font-provider.tsx, config/fonts.ts, --font-inter/--font-manrope)
- Typography contract: 3-tier headings, arbitrary text-[10px]/[11px] → text-xs (46 files), no uppercase table headers, font-bold → font-semibold
- Numbers use tabular-nums only; font-mono reserved for keys/IDs/code (29 files)

🎨 Color discipline
- Retire StatusBadge string-hash rainbow (autoColor → neutral); all variants resolve to 5 semantic voices
- Add tintedBadgeClassMap as the single tinted-pill recipe
- Replace hardcoded emerald/amber/rose/sky classes with semantic tokens across ~30 files (timing badges, progress bars, system panels, pricing, rankings, log row tints)
- Drop all !important text overrides and [font-family:var(--font-body)] hacks

🧘 Motion restraint
- Remove admin entrance animations: table row stagger, card hover lift/shadow, button press scale, dashboard shimmer line
- Neuter page-transition stagger components into plain wrappers; page transitions reduced to pure opacity fade
- Decorative motion stays landing-only

📐 Shape
- Single radius source: --radius 1rem → 0.625rem
- Sparklines use solid chart tokens instead of decorative gradients
- PanelWrapper: border-only (no shadow), align radius with Card

📏 Guardrails
- Add "Design System Discipline" section (3.10.1) to web/default/AGENTS.md

 Verified: tsgo typecheck clean, rsbuild production build passes
This commit is contained in:
t0ng7u
2026-07-11 02:02:11 +08:00
parent 0cb741d8da
commit 262ab93123
97 changed files with 561 additions and 863 deletions
+36
View File
@@ -125,6 +125,41 @@
- 以 Tailwind 工具类为主,动态类名用 `cn()` 合并;非动态场景避免内联样式。
- 响应式采用移动优先与 Tailwind 断点(`sm:``md:``lg:` 等);主题与暗色用 CSS 变量与 `dark:`,自定义样式集中在 `src/styles/`,组件内尽量少写自定义 CSS。
#### 3.10.1 设计系统纪律(强约束)
以下规则为硬约束,代码评审与 AI 改动一律遵循;违反即为缺陷。
**颜色**
- 后台界面(除 `features/home/` 营销页外)**禁止硬编码 Tailwind 调色板类**(如 `text-emerald-600``bg-amber-50``border-rose-200`),一律使用语义令牌:`success` / `warning` / `destructive` / `info` / `neutral``primary` / `muted` / `accent` / `border` / `ring`。暗色适配由令牌完成,禁止 `dark:text-emerald-400` 之类逐处覆写。
- 状态色仅表达状态(成功/警告/错误/信息),不得用于装饰或分类;分类信息(模型名、分组名、渠道名等)一律中性呈现,身份靠文本与图标传达。`StatusBadge``autoColor` 已废弃为 neutral,禁止恢复字符串哈希取色。
- 需要浅底状态胶囊时使用 `status-badge.tsx``tintedBadgeClassMap`,不要再手写 `bg-xxx-50 text-xxx-700 dark:...` 组合。
- 图表用 `--chart-1..5`;用户头像身份色走 `getAvatarColorClass`。这两处是仅有的多色场景。
- 禁止用 `!important``!text-*` 等)压制文字颜色/字号;若需要覆盖,说明层级设计有误,先修组件。
**排版**
- 字体族只有两轨:正文 `--font-sans`(含 CJK 回退,勿删栈中中文字体)与 `--font-serif`serif 主题轴)。`font-mono` 仅用于密钥、ID、代码、原始 JSON;**数字对齐一律 `tabular-nums`,不得再加 `font-mono`**。
- 字号只用 `text-xs/sm/base/lg/xl/2xl`**禁止任意值字号**`text-[10px]``text-[11px]` 等;营销页除外)。
- 字重只用 `font-medium`(强调/表头)与 `font-semibold`(标题/关键数值);后台正文与标题**禁用 `font-bold`**。
- 标题三档契约:页题 `text-lg font-semibold tracking-tight`;卡片/区块题 `text-base font-semibold`;面板/小节题 `text-sm font-medium`。禁止响应式跳字号的标题(如 `text-base sm:text-lg`)。
- 表头不使用 `uppercase + tracking-wider`
**圆角与阴影**
- 圆角单源 `--radius`(默认 0.625rem),组件用派生档 `rounded-md/lg/xl`;禁止在业务代码里写死圆角像素或混用 `rounded-2xl`/`rounded-4xl` 表达同类容器。
- 卡片边界「描边或投影二选一」:默认 `border`(或 Card 自带 ring),禁止再叠 `shadow-*``shadow` 只保留给浮层(popover/dropdown/dialog)。
**动效**
- 后台界面禁止入场动画:无 stagger、无 translate/scale/blur 入场、无按钮按压缩放。页面切换只允许纯透明度 fade(见 `lib/motion.ts``pageEnter`)。`page-transition.tsx` 的 Stagger 系列组件已固化为纯容器,禁止恢复动画。
- 允许的动效上限:颜色/透明度过渡 ≤150ms、骨架屏 shimmer、`Collapsible/Accordion` 展开、加载 spinner。装饰性动效只允许出现在 `features/home/`landing)。
**徽章与图标**
- 文本徽章统一 `StatusBadge`(五种语义 voice);模型/分组/渠道等实体徽章统一中性底(`border-border/60 bg-muted/30`)。不要新造第三种徽章样式。
- 业务代码图标一律 `lucide-react``components/ui/` 由 shadcn 生成器维护(Hugeicons),不要手改基础组件图标库;AI 品牌图标用 `@lobehub/icons`(经 `getLobeIcon`)。
### 3.11 文件组织
- **功能模块**:置于 `src/features/<feature>/`,内含 `components/``lib/``hooks/`,以及按需的 `api.ts``types.ts``constants.ts`、入口组件等。
@@ -178,3 +213,4 @@
- **2026-01-29**:重组文档结构,合并重复内容,明确主次与交叉引用。
- **2026-01-31**:在 3.2 中补充「类型检查」要求:改动 TS/TSX 后须执行 typecheck 并修复至无错。
- **2026-06-21**:在 3.2 中补充「Lint 检查」要求:完成代码改动前须修复所涉及文件的所有 lint error。
- **2026-07-11**:新增 3.10.1「设计系统纪律」:语义色硬约束(去彩虹徽章/硬编码调色板)、排版契约(字号/字重/标题三档/mono 边界)、圆角单源、后台零入场动效、徽章与图标单轨。
+1 -1
View File
@@ -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-[11px] font-medium tracking-wide uppercase'>
<div className='text-muted-foreground truncate font-mono text-xs font-medium tracking-wide uppercase'>
{title}
</div>
</div>
@@ -110,7 +110,7 @@ function CompactContent<TData>({ row }: { row: Row<TData> }) {
return (
<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'>
<div className='text-muted-foreground mb-0.5 text-xs leading-none select-none'>
{label}
</div>
)}
@@ -177,7 +177,7 @@ function FallbackContent<TData>({ row }: { row: Row<TData> }) {
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'>
<span className='text-muted-foreground shrink-0 text-xs font-medium select-none'>
{label}
</span>
<div className='flex min-w-0 flex-1 items-center justify-end overflow-hidden text-xs'>
@@ -24,10 +24,9 @@ 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-[10px] font-medium tracking-wider uppercase',
compactHeaderCell: 'text-muted-foreground py-2 text-xs font-medium',
compactHeaderCellRight:
'text-muted-foreground py-2 text-right text-[10px] font-medium tracking-wider uppercase',
'text-muted-foreground py-2 text-right text-xs font-medium',
compactCell: 'py-2.5',
compactTopCell: 'py-2.5 align-top',
compactTopNumericCell: 'py-2.5 text-right align-top font-mono',
+1 -1
View File
@@ -98,7 +98,7 @@ export function GroupBadge(props: GroupBadgeProps) {
<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',
'inline-flex h-5 shrink-0 items-center rounded-full px-1.5 text-xs leading-none font-medium tabular-nums',
getGroupRatioClassName(ratio)
)}
>
+2 -8
View File
@@ -97,10 +97,7 @@ 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')
@@ -182,10 +179,7 @@ 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)} />
)
}
+1 -1
View File
@@ -229,7 +229,7 @@ export function JsonCodeEditor({
<span
className={cn(
'flex items-center gap-1 text-xs',
jsonStatus.valid ? 'text-emerald-600' : 'text-destructive'
jsonStatus.valid ? 'text-success' : 'text-destructive'
)}
>
{jsonStatus.valid ? (
+1 -1
View File
@@ -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-[11px] font-medium tracking-wider uppercase'>
<SidebarGroupLabel className='text-muted-foreground/70 px-2 text-xs font-medium tracking-wider uppercase'>
{title}
</SidebarGroupLabel>
<SidebarMenu>
@@ -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-base font-bold tracking-tight sm:text-lg'>
<h2 className='truncate text-lg font-semibold tracking-tight'>
{title}
</h2>
</div>
+6 -6
View File
@@ -253,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-[10px]'
isMobile ? 'text-xs' : 'text-xs'
)}
>
{t('{{category}} Models', { category })}
@@ -274,7 +274,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = React.memo(
<div
className={cn(
'truncate font-medium',
isMobile ? 'text-sm' : 'text-[11px]'
isMobile ? 'text-sm' : 'text-xs'
)}
>
<span className='inline'>{model.label}</span>
@@ -400,7 +400,7 @@ export const GroupSelector: React.FC<GroupSelectorProps> = React.memo(
className={isMobile ? '!max-h-full flex-1 p-2' : 'max-h-[240px]'}
>
<CommandGroup>
<div className='text-muted-foreground px-2 py-1 text-[10px] font-medium'>
<div className='text-muted-foreground px-2 py-1 text-xs font-medium'>
{t('Model Group')}
</div>
{groups.map((group) => (
@@ -417,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-[11px] font-medium'>
<span className='text-foreground truncate text-xs font-medium'>
{group.label}
</span>
{(group.desc || group.description) && (
@@ -658,7 +658,7 @@ export const ModelGroupSelector: React.FC<ModelGroupSelectorProps> = ({
<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-[10px] sm:inline-flex'>
<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'>
{currentGroup?.label || t('Group')}
</span>
<ChevronsUpDown className='text-muted-foreground ml-auto size-3.5 shrink-0 opacity-60' />
@@ -672,7 +672,7 @@ export const ModelGroupSelector: React.FC<ModelGroupSelectorProps> = ({
!isMobile && modelGroupSelectorLayoutClasses.groupColumn
)}
>
<div className='text-muted-foreground px-1 text-[11px] leading-4 font-medium'>
<div className='text-muted-foreground px-1 text-xs leading-4 font-medium'>
{t('Model Group')}
</div>
<div
+1 -1
View File
@@ -318,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-[10px] font-semibold tabular-nums'
className='absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center px-1 text-xs font-semibold tabular-nums'
>
{unreadCount > 99 ? '99+' : unreadCount}
</Badge>
+12 -76
View File
@@ -20,16 +20,7 @@ import { Outlet, useRouterState } from '@tanstack/react-router'
import { motion, useReducedMotion, type Variants } from 'motion/react'
import type { ReactNode } from 'react'
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'
import { MOTION_TRANSITION, MOTION_VARIANTS } from '@/lib/motion'
interface PageTransitionProps {
children: ReactNode
@@ -92,23 +83,13 @@ 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) {
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>
)
return <div className={props.className}>{props.children}</div>
}
interface StaggerItemProps {
@@ -118,68 +99,23 @@ interface StaggerItemProps {
}
export function StaggerItem(props: StaggerItemProps) {
return (
<motion.div
variants={props.variants ?? STAGGER_ITEM_VARIANTS}
className={props.className}
>
{props.children}
</motion.div>
)
return <div className={props.className}>{props.children}</div>
}
export function TableStaggerContainer(props: StaggerContainerProps) {
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>
)
return <tbody className={props.className}>{props.children}</tbody>
}
export function TableStaggerRow(props: StaggerItemProps) {
return (
<motion.tr variants={TABLE_ROW_VARIANTS} className={props.className}>
{props.children}
</motion.tr>
)
return <tr className={props.className}>{props.children}</tr>
}
export function CardStaggerContainer(props: StaggerContainerProps) {
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>
)
return <div className={props.className}>{props.children}</div>
}
export function CardStaggerItem(props: StaggerItemProps) {
return (
<motion.div variants={CARD_ITEM_VARIANTS} className={props.className}>
{props.children}
</motion.div>
)
return <div className={props.className}>{props.children}</div>
}
interface FadeInProps {
+1 -1
View File
@@ -63,7 +63,7 @@ export function ProfileDropdown() {
>
<Avatar className='size-6'>
<AvatarFallback
className={`${avatarFallbackClassName} text-[11px]`}
className={`${avatarFallbackClassName} text-xs`}
style={avatarFallbackStyle}
>
{avatarFallback}
+1 -4
View File
@@ -16,10 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import {
HtmlContent,
type HtmlContentVariant,
} from '@/components/html-content'
import { HtmlContent, type HtmlContentVariant } from '@/components/html-content'
import { Markdown } from '@/components/ui/markdown'
type RichContentMode = 'markdown' | 'html'
+1 -1
View File
@@ -50,7 +50,7 @@ export function Search({ className = '', placeholder }: SearchProps) {
size={16}
/>
<span className='ms-4'>{resolvedPlaceholder}</span>
<kbd className='bg-muted group-hover:bg-accent pointer-events-none absolute end-[0.3rem] top-[0.3rem] hidden h-5 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 select-none sm:flex'>
<kbd className='bg-muted group-hover:bg-accent pointer-events-none absolute end-[0.3rem] top-[0.3rem] hidden h-5 items-center gap-1 rounded border px-1.5 font-mono text-xs font-medium opacity-100 select-none sm:flex'>
<span className='text-xs'></span>
{t('K')}
</kbd>
+42 -23
View File
@@ -21,30 +21,34 @@ For commercial licensing, please contact support@quantumnous.com
import * as React from 'react'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { stringToColor } from '@/lib/colors'
import { cn } from '@/lib/utils'
/* Color discipline: badges speak exactly five voices — success / warning /
* danger / info / neutral. Legacy hue keys (blue, purple, teal, ...) remain
* valid variant names for compatibility, but they all resolve to one of the
* five semantic tokens so tables stop reading as rainbows. Chart hues stay
* reserved for data visualization. */
export const dotColorMap = {
success: 'bg-success',
warning: 'bg-warning',
danger: 'bg-destructive',
info: 'bg-info',
neutral: 'bg-neutral',
purple: 'bg-chart-4',
purple: 'bg-neutral',
amber: 'bg-warning',
blue: 'bg-chart-1',
cyan: 'bg-chart-2',
blue: 'bg-neutral',
cyan: 'bg-neutral',
green: 'bg-success',
grey: 'bg-neutral',
indigo: 'bg-chart-1',
indigo: 'bg-neutral',
'light-blue': 'bg-info',
'light-green': 'bg-emerald-400',
lime: 'bg-chart-3',
'light-green': 'bg-success',
lime: 'bg-neutral',
orange: 'bg-warning',
pink: 'bg-chart-5',
pink: 'bg-neutral',
red: 'bg-destructive',
teal: 'bg-chart-2',
violet: 'bg-chart-4',
teal: 'bg-neutral',
violet: 'bg-neutral',
yellow: 'bg-warning',
} as const
@@ -54,26 +58,38 @@ export const textColorMap = {
danger: 'text-destructive',
info: 'text-info',
neutral: 'text-muted-foreground',
purple: 'text-chart-4',
purple: 'text-muted-foreground',
amber: 'text-warning',
blue: 'text-chart-1',
cyan: 'text-chart-2',
blue: 'text-muted-foreground',
cyan: 'text-muted-foreground',
green: 'text-success',
grey: 'text-muted-foreground',
indigo: 'text-chart-1',
indigo: 'text-muted-foreground',
'light-blue': 'text-info',
'light-green': 'text-emerald-500 dark:text-emerald-300',
lime: 'text-chart-3',
'light-green': 'text-success',
lime: 'text-muted-foreground',
orange: 'text-warning',
pink: 'text-chart-5',
pink: 'text-muted-foreground',
red: 'text-destructive',
teal: 'text-chart-2',
violet: 'text-chart-4',
teal: 'text-muted-foreground',
violet: 'text-muted-foreground',
yellow: 'text-warning',
} as const
export type StatusVariant = keyof typeof dotColorMap
/** Soft tinted pill surface per semantic voice — the one sanctioned recipe
* for badges that need a filled background (timing pills, duration pills).
* Border/background/text always come from the same token so light and dark
* modes stay consistent without `!important` overrides. */
export const tintedBadgeClassMap = {
success: 'border border-success/25 bg-success/10 text-success',
warning: 'border border-warning/30 bg-warning/10 text-warning',
danger: 'border border-destructive/25 bg-destructive/10 text-destructive',
info: 'border border-info/25 bg-info/10 text-info',
neutral: 'border border-border/60 bg-muted/30 text-muted-foreground',
} as const
/** Controls the visual style of the badge.
* - `badge` — default pill with background and padding (default)
* - `text` — plain text, no background or padding, only color
@@ -112,6 +128,8 @@ export interface StatusBadgeProps extends Omit<
size?: 'sm' | 'md' | 'lg' | null
copyable?: boolean
copyText?: string
/** Deprecated: categorical identity is conveyed by label/icon, not hue.
* Accepted for call-site compatibility; renders as `neutral`. */
autoColor?: string
/** Visual style. Defaults to 'badge'. Can be overridden via StatusBadgeTypeContext. */
type?: StatusBadgeType
@@ -137,9 +155,10 @@ export function StatusBadge({
const contextType = React.useContext(StatusBadgeTypeContext)
const type = typeProp ?? contextType
const computedVariant: StatusVariant = autoColor
? (stringToColor(autoColor) as StatusVariant)
: (variant ?? 'neutral')
// String-hash rainbow coloring is retired; `autoColor` intentionally falls
// through to the neutral voice (see prop doc above).
void autoColor
const computedVariant: StatusVariant = variant ?? 'neutral'
const handleClick = (e: React.MouseEvent<HTMLSpanElement>) => {
if (copyable) {
@@ -166,7 +185,7 @@ export function StatusBadge({
className={cn(
'inline-flex w-fit max-w-full min-w-0 shrink items-center font-medium tracking-normal whitespace-nowrap transition-colors',
isBadge
? cn('rounded-4xl', sizeMap[size ?? 'sm'])
? cn('rounded-md', sizeMap[size ?? 'sm'])
: cn(
textSizeMap[size ?? 'sm'],
type === 'underline' && 'border-b border-current pb-px'
+1 -1
View File
@@ -27,7 +27,7 @@ export function TableId(props: TableIdProps) {
return (
<span
className={cn(
'text-muted-foreground inline-block font-mono tabular-nums',
'text-muted-foreground inline-block tabular-nums',
props.className
)}
>
+1 -1
View File
@@ -269,7 +269,7 @@ function ChartTooltipContent({
</span>
</div>
{item.value != null && (
<span className='text-foreground font-mono font-medium tabular-nums'>
<span className='text-foreground font-medium tabular-nums'>
{typeof item.value === 'number'
? item.value.toLocaleString()
: String(item.value)}
+4 -4
View File
@@ -63,7 +63,7 @@ export function TitledCard({
className={cn('gap-0 overflow-hidden py-0', className)}
>
<CardHeader
className={cn('border-b p-3 !pb-3 sm:p-5 sm:!pb-5', headerClassName)}
className={cn('border-b p-4 !pb-4 sm:p-5 sm:!pb-5', headerClassName)}
>
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
<div className='flex min-w-0 items-center gap-3'>
@@ -80,7 +80,7 @@ export function TitledCard({
<div className='min-w-0'>
<CardTitle
className={cn(
'text-lg tracking-tight sm:text-xl',
'text-base font-semibold tracking-tight',
titleClassName
)}
>
@@ -88,7 +88,7 @@ export function TitledCard({
</CardTitle>
{description != null && (
<CardDescription
className={cn('text-xs sm:text-sm', descriptionClassName)}
className={cn('text-sm', descriptionClassName)}
>
{description}
</CardDescription>
@@ -100,7 +100,7 @@ export function TitledCard({
)}
</div>
</CardHeader>
<CardContent className={cn('p-3 sm:p-5', contentClassName)}>
<CardContent className={cn('p-4 sm:p-5', contentClassName)}>
{children}
</CardContent>
</Card>
-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
*/
/**
* List of available font names (visit the url `/settings/appearance`).
* This array is used to generate dynamic font classes (e.g., `font-inter`, `font-manrope`).
*
* 📝 How to Add a New Font (Tailwind v4+):
* 1. Add the font name here.
* 2. Update the `<link>` tag in 'index.html' to include the new font from CDN or other source.
* 3. Add the new font family to 'theme.css' using the `@theme inline` and `font-family` CSS variable.
*
* Example:
* fonts.ts → Add 'roboto' to this array.
* index.html → Add font link for Roboto.
* theme.css → Add the new font in the CSS, e.g.:
* @theme inline {
* // ... other font families
* --font-roboto: 'Roboto', var(--font-sans);
* }
*/
export const fonts = ['inter', 'manrope', 'system'] as const
-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 { createContext, useContext, useEffect, useState } from 'react'
import { fonts } from '@/config/fonts'
import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
type Font = (typeof fonts)[number]
const FONT_COOKIE_NAME = 'font'
const FONT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
type FontContextType = {
font: Font
setFont: (font: Font) => void
resetFont: () => void
}
const FontContext = createContext<FontContextType | null>(null)
export function FontProvider({ children }: { children: React.ReactNode }) {
const [font, _setFont] = useState<Font>(() => {
const savedFont = getCookie(FONT_COOKIE_NAME)
return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0]
})
useEffect(() => {
const applyFont = (font: string) => {
const root = document.documentElement
root.classList.forEach((cls) => {
if (cls.startsWith('font-')) root.classList.remove(cls)
})
root.classList.add(`font-${font}`)
}
applyFont(font)
}, [font])
const setFont = (font: Font) => {
setCookie(FONT_COOKIE_NAME, font, FONT_COOKIE_MAX_AGE)
_setFont(font)
}
const resetFont = () => {
removeCookie(FONT_COOKIE_NAME)
_setFont(fonts[0])
}
return (
<FontContext value={{ font, setFont, resetFont }}>{children}</FontContext>
)
}
// eslint-disable-next-line react-refresh/only-export-components
export const useFont = () => {
const context = useContext(FontContext)
if (!context) {
throw new Error('useFont must be used within a FontProvider')
}
return context
}
@@ -78,7 +78,7 @@ function ChannelCardComponent({
const responseCell = renderCell('response_time')
const testCell = renderCell('test_time')
const labelClass = 'text-muted-foreground text-[11px] font-medium select-none'
const labelClass = 'text-muted-foreground text-xs font-medium select-none'
// In card view the enable/disable state is already conveyed by the inline
// power toggle, so the plain "Enabled"/"Disabled" badge is redundant. Keep
@@ -638,7 +638,7 @@ export function useChannelsColumns(
<Tooltip>
<TooltipTrigger
render={
<AlertTriangle className='h-3.5 w-3.5 flex-shrink-0 text-amber-500' />
<AlertTriangle className='text-warning h-3.5 w-3.5 flex-shrink-0' />
}
/>
<TooltipContent side='top'>
@@ -30,7 +30,11 @@ import { useTranslation } from 'react-i18next'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { Dialog } from '@/components/dialog'
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
import {
StatusBadge,
textColorMap,
type StatusBadgeProps,
} from '@/components/status-badge'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
@@ -400,32 +404,11 @@ function formatLabelValue(label: string, value: string) {
return label.endsWith('') ? `${label}${value}` : `${label} ${value}`
}
// Single source of truth for variant → text color lives in status-badge.
const percentTextClassName: Record<
NonNullable<StatusBadgeProps['variant']>,
string
> = {
success: 'text-success',
warning: 'text-warning',
danger: 'text-destructive',
info: 'text-info',
neutral: 'text-muted-foreground',
purple: 'text-chart-4',
amber: 'text-warning',
blue: 'text-chart-1',
cyan: 'text-chart-2',
green: 'text-success',
grey: 'text-muted-foreground',
indigo: 'text-chart-1',
'light-blue': 'text-info',
'light-green': 'text-emerald-500 dark:text-emerald-300',
lime: 'text-chart-3',
orange: 'text-warning',
pink: 'text-chart-5',
red: 'text-destructive',
teal: 'text-chart-2',
violet: 'text-chart-4',
yellow: 'text-warning',
}
> = textColorMap
type RateLimitWindowProps = {
title: string
@@ -464,7 +447,7 @@ function RateLimitWindow(props: RateLimitWindowProps) {
>
{hasData ? `${percent}%` : '-'}
</div>
<div className='text-muted-foreground mt-1 text-[11px]'>
<div className='text-muted-foreground mt-1 text-xs'>
{t('Used')}
</div>
</div>
@@ -482,7 +465,7 @@ function RateLimitWindow(props: RateLimitWindowProps) {
)}
<div className='mt-3 grid grid-cols-1 gap-2 text-xs sm:grid-cols-2'>
<div className='min-w-0'>
<div className='text-muted-foreground text-[11px]'>
<div className='text-muted-foreground text-xs'>
{t('Reset at:')}
</div>
<div className='break-all tabular-nums'>
@@ -490,7 +473,7 @@ function RateLimitWindow(props: RateLimitWindowProps) {
</div>
</div>
<div className='min-w-0 sm:text-right'>
<div className='text-muted-foreground text-[11px]'>
<div className='text-muted-foreground text-xs'>
{t('Resets in:')}
</div>
<div className='tabular-nums'>
@@ -565,9 +548,7 @@ function RateLimitGroupSection(props: RateLimitGroupSectionProps) {
</SectionHeading>
{props.meteredFeature ? (
<div className='bg-background ring-border/60 inline-flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 rounded-lg px-2 py-1 text-xs ring-1'>
<span className='text-muted-foreground text-[11px]'>
metered_feature
</span>
<span className='text-muted-foreground text-xs'>metered_feature</span>
<span className='min-w-0 font-mono break-all'>
{props.meteredFeature}
</span>
@@ -600,7 +581,7 @@ function InfoField(props: {
props.className
)}
>
<div className='text-muted-foreground text-[11px] font-medium'>
<div className='text-muted-foreground text-xs font-medium'>
{props.label}
</div>
<div className='mt-1 flex min-w-0 items-start justify-between gap-2'>
@@ -635,7 +616,7 @@ function ResetCreditTimeField(props: {
}) {
return (
<div className='min-w-0'>
<div className='text-muted-foreground text-[11px] font-medium'>
<div className='text-muted-foreground text-xs font-medium'>
{props.label}
</div>
<div
@@ -679,13 +660,13 @@ function ResetCreditItem(props: { credit: CodexResetCredit; index: number }) {
</div>
) : null}
{props.credit.id ? (
<div className='text-muted-foreground mt-1 font-mono text-[11px] break-all'>
<div className='text-muted-foreground mt-1 font-mono text-xs break-all'>
{props.credit.id}
</div>
) : null}
</div>
<div className='shrink-0 text-right'>
<div className='text-muted-foreground text-[11px] font-medium'>
<div className='text-muted-foreground text-xs font-medium'>
{t('Expires in')}
</div>
<div
@@ -344,7 +344,7 @@ export function FetchModelsDialog({
{redirectOnlySet.has(normalizeModelName(model)) && (
<Tooltip>
<TooltipTrigger
render={<Info className='h-3.5 w-3.5 text-amber-500' />}
render={<Info className='text-warning h-3.5 w-3.5' />}
></TooltipTrigger>
<TooltipContent>
{t('From model redirect, not yet added to models list')}
@@ -553,17 +553,11 @@ const getOperationSummary = (
}
const getModeTagTailwind = (mode: string): string => {
if (mode.includes('header'))
return 'bg-cyan-500/15 text-cyan-700 dark:text-cyan-300 border-cyan-500/20'
if (mode.includes('replace') || mode.includes('trim'))
return 'bg-violet-500/15 text-violet-700 dark:text-violet-300 border-violet-500/20'
if (mode.includes('copy') || mode.includes('move'))
return 'bg-blue-500/15 text-blue-700 dark:text-blue-300 border-blue-500/20'
// Only destructive-ish operations get a color voice; the rest stay neutral —
// the mode label itself carries the identity.
if (mode.includes('error') || mode.includes('prune'))
return 'bg-red-500/15 text-red-700 dark:text-red-300 border-red-500/20'
if (mode.includes('sync'))
return 'bg-green-500/15 text-green-700 dark:text-green-300 border-green-500/20'
return 'bg-muted text-muted-foreground'
return 'bg-destructive/10 text-destructive border-destructive/20'
return 'bg-muted text-muted-foreground border-border/60'
}
const getModePathLabel = (mode: string): string => {
@@ -1849,7 +1843,7 @@ export function ParamOverrideEditorDialog(
<span
key={`mode_stat_${mode}`}
className={cn(
'inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium',
'inline-flex items-center rounded-md border px-1.5 py-0.5 text-xs font-medium',
getModeTagTailwind(mode)
)}
>
@@ -1935,24 +1929,21 @@ export function ParamOverrideEditorDialog(
<span className='text-xs font-semibold'>
#{index + 1}
</span>
<Badge
variant='outline'
className='text-[10px]'
>
<Badge variant='outline' className='text-xs'>
{operation.conditions.length}
</Badge>
</div>
<p className='text-muted-foreground mt-0.5 line-clamp-1 text-[11px]'>
<p className='text-muted-foreground mt-0.5 line-clamp-1 text-xs'>
{getOperationSummary(operation, index)}
</p>
{operation.description.trim() && (
<p className='text-muted-foreground mt-0.5 line-clamp-2 text-[10px]'>
<p className='text-muted-foreground mt-0.5 line-clamp-2 text-xs'>
{operation.description}
</p>
)}
<span
className={cn(
'mt-1 inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium',
'mt-1 inline-flex items-center rounded-md border px-1.5 py-0.5 text-xs font-medium',
getModeTagTailwind(operation.mode || 'set')
)}
>
@@ -2214,7 +2205,7 @@ function RuleEditor(ruleEditorProps: RuleEditorProps) {
<label className='text-xs font-medium'>
{t('Rule Description (optional)')}
</label>
<span className='text-muted-foreground text-[10px]'>
<span className='text-muted-foreground text-xs'>
{operation.description.length}/180
</span>
</div>
@@ -2485,7 +2476,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
<div className='rounded-md border'>
<CollapsibleTrigger className='hover:bg-muted/50 flex w-full items-center justify-between px-3 py-2'>
<div className='flex items-center gap-2'>
<Badge variant='outline' className='text-[10px]'>
<Badge variant='outline' className='text-xs'>
C{conditionEditorProps.conditionIndex + 1}
</Badge>
<span className='text-muted-foreground text-xs'>
@@ -2522,9 +2513,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
</div>
<div className='grid gap-2 sm:grid-cols-3'>
<div className='space-y-1'>
<label className='text-[10px] font-medium'>
{t('Field Path')}
</label>
<label className='text-xs font-medium'>{t('Field Path')}</label>
<Input
value={condition.path}
onChange={(e) =>
@@ -2539,9 +2528,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
/>
</div>
<div className='space-y-1'>
<label className='text-[10px] font-medium'>
{t('Match Mode')}
</label>
<label className='text-xs font-medium'>{t('Match Mode')}</label>
<Select
items={[
...CONDITION_MODE_OPTIONS.map((o) => ({
@@ -2574,7 +2561,7 @@ function ConditionEditor(conditionEditorProps: ConditionEditorProps) {
</Select>
</div>
<div className='space-y-1'>
<label className='text-[10px] font-medium'>
<label className='text-xs font-medium'>
{t('Match Value')}
</label>
<Input
@@ -2815,7 +2802,7 @@ function ReturnErrorEditor(returnErrorEditorProps: ReturnErrorEditorProps) {
type='button'
variant='outline'
size='sm'
className='h-6 text-[10px]'
className='h-6 text-xs'
onClick={() =>
returnErrorEditorProps.updateDraft(
returnErrorEditorProps.operationId,
@@ -3021,14 +3008,14 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
className='bg-background rounded-md border p-2'
>
<div className='mb-1 flex items-center justify-between'>
<Badge variant='outline' className='text-[10px]'>
<Badge variant='outline' className='text-xs'>
R{ruleIndex + 1}
</Badge>
<Button
type='button'
variant='ghost'
size='sm'
className='text-destructive hover:text-destructive h-6 text-[10px]'
className='text-destructive hover:text-destructive h-6 text-xs'
onClick={() =>
pruneObjectsEditorProps.removeRule(
pruneObjectsEditorProps.operationId,
@@ -3042,7 +3029,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
</div>
<div className='grid gap-2 sm:grid-cols-3'>
<div className='space-y-0.5'>
<label className='text-[10px] font-medium'>
<label className='text-xs font-medium'>
{t('Field Path')}
</label>
<Input
@@ -3059,7 +3046,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
/>
</div>
<div className='space-y-0.5'>
<label className='text-[10px] font-medium'>
<label className='text-xs font-medium'>
{t('Match Mode')}
</label>
<Select
@@ -3094,7 +3081,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
</Select>
</div>
<div className='space-y-0.5'>
<label className='text-[10px] font-medium'>
<label className='text-xs font-medium'>
{t('Match Value (optional)')}
</label>
<Input
@@ -3112,7 +3099,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
</div>
</div>
<div className='mt-1.5 flex flex-wrap gap-3'>
<label className='flex items-center gap-1.5 text-[10px]'>
<label className='flex items-center gap-1.5 text-xs'>
<Switch
checked={rule.invert}
onCheckedChange={(checked) =>
@@ -3125,7 +3112,7 @@ function PruneObjectsEditor(pruneObjectsEditorProps: PruneObjectsEditorProps) {
/>
{t('Invert match')}
</label>
<label className='flex items-center gap-1.5 text-[10px]'>
<label className='flex items-center gap-1.5 text-xs'>
<Switch
checked={rule.pass_missing_key}
onCheckedChange={(checked) =>
@@ -3171,9 +3158,7 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
<label className='text-xs font-medium'>{t('Sync Endpoints')}</label>
<div className='grid gap-3 sm:grid-cols-2'>
<div className='space-y-1.5'>
<label className='text-[10px] font-medium'>
{t('Source Endpoint')}
</label>
<label className='text-xs font-medium'>{t('Source Endpoint')}</label>
<div className='flex gap-2'>
<Select
items={[
@@ -3228,9 +3213,7 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
</div>
</div>
<div className='space-y-1.5'>
<label className='text-[10px] font-medium'>
{t('Target Endpoint')}
</label>
<label className='text-xs font-medium'>{t('Target Endpoint')}</label>
<div className='flex gap-2'>
<Select
items={[
@@ -3303,7 +3286,7 @@ function SyncFieldsEditor(syncFieldsEditorProps: SyncFieldsEditorProps) {
type='button'
variant='outline'
size='sm'
className='h-6 text-[10px]'
className='h-6 text-xs'
onClick={() =>
syncFieldsEditorProps.updateOperation(
syncFieldsEditorProps.operationId,
@@ -1859,7 +1859,7 @@ export function ChannelMutateDrawer({
</SheetHeader>
{sensitiveLocked && (
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
<Alert className='border-warning/40 bg-warning/10'>
<AlertDescription>
{t(
'Sensitive channel settings are read-only for your account.'
@@ -2077,7 +2077,7 @@ export function ChannelMutateDrawer({
)}
{sensitiveLocked && (
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
<Alert className='border-warning/40 bg-warning/10'>
<AlertDescription>
{t('No permission to perform this action')}
</AlertDescription>
@@ -3068,7 +3068,7 @@ export function ChannelMutateDrawer({
)}
</div>
</div>
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
<Alert className='border-warning/40 bg-warning/10'>
<AlertDescription>
{t(
"Disclaimer: Personal use only. Do not distribute or share any credentials. This channel has prerequisites and requires prior setup; use it only if you understand the flow and risks, and comply with OpenAI's terms and policies. Credentials and configuration are for Codex CLI integration only, and are not intended for any other client, platform, or channel."
@@ -3244,7 +3244,7 @@ export function ChannelMutateDrawer({
</FormControl>
{modelMappingGuardrail.exposedTargetModels
.length > 0 && (
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
<Alert className='border-warning/40 bg-warning/10'>
<AlertDescription className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
<span>
{t('The mapped upstream model(s)')}{' '}
@@ -3451,7 +3451,7 @@ export function ChannelMutateDrawer({
)
)}
{remainingMappingCount > 0 && (
<div className='text-[11px] opacity-70'>
<div className='text-xs opacity-70'>
+{remainingMappingCount}{' '}
{t('more mapping')}
{remainingMappingCount > 1
@@ -3460,7 +3460,7 @@ export function ChannelMutateDrawer({
</div>
)}
</div>
<p className='text-[11px] leading-relaxed opacity-80'>
<p className='text-xs leading-relaxed opacity-80'>
{t(
'Users call the model on the left. The platform forwards the request to the upstream model on the right.'
)}
@@ -3501,7 +3501,7 @@ export function ChannelMutateDrawer({
)}
{modelMappingGuardrail.missingSourceModels
.length > 0 && (
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
<Alert className='border-warning/40 bg-warning/10'>
<AlertDescription className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
<span>
{t('Add')}{' '}
@@ -4026,7 +4026,7 @@ export function ChannelMutateDrawer({
icon={<Settings className='h-4 w-4' />}
/>
{sensitiveLocked && (
<Alert className='border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-50'>
<Alert className='border-warning/40 bg-warning/10'>
<AlertDescription>
{t('No permission to perform this action')}
</AlertDescription>
@@ -167,7 +167,7 @@ export function NumericSpinnerInput({
disabled={disabled}
title={localValue}
className={cn(
'h-7 min-w-8 max-w-16 cursor-text truncate px-1 text-center font-mono text-sm tabular-nums',
'h-7 min-w-8 max-w-16 cursor-text truncate px-1 text-center text-sm tabular-nums',
disabled && 'cursor-default opacity-50'
)}
>
@@ -169,7 +169,7 @@ export function LogStatCards(props: LogStatCardsProps) {
</div>
) : error ? (
<>
<div className='text-muted-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
<div className='text-muted-foreground mt-1.5 text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
--
</div>
<div className='text-muted-foreground/40 mt-1 hidden text-xs md:block'>
@@ -179,7 +179,7 @@ export function LogStatCards(props: LogStatCardsProps) {
) : (
<>
<div
className='text-foreground mt-1.5 max-w-full truncate font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'
className='text-foreground mt-1.5 max-w-full truncate text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'
title={it.fullValue}
>
{it.value}
@@ -186,10 +186,10 @@ function InlineMetric(props: {
className='text-muted-foreground/50 size-3 shrink-0'
aria-hidden='true'
/>
<span className='text-muted-foreground text-[11px]'>{props.label}</span>
<span className='text-muted-foreground text-xs'>{props.label}</span>
<span
className={cn(
'font-mono text-xs font-semibold tabular-nums',
'text-xs font-semibold tabular-nums',
props.valueClassName
)}
>
@@ -204,7 +204,7 @@ function ModelBadge(props: { model: PerfModelSummary }) {
return (
<span className='bg-muted/50 inline-flex items-center gap-1.5 rounded-full px-2.5 py-1'>
<span className='max-w-[10rem] truncate font-mono text-[11px]'>
<span className='max-w-[10rem] truncate font-mono text-xs'>
{model.model_name}
</span>
<span
@@ -216,7 +216,7 @@ function ModelBadge(props: { model: PerfModelSummary }) {
/>
<span
className={cn(
'font-mono text-[11px] font-semibold tabular-nums',
'text-xs font-semibold tabular-nums',
getSuccessRateTextClass(model.success_rate)
)}
>
@@ -36,7 +36,6 @@ import {
Timer,
type LucideIcon,
} from 'lucide-react'
import { motion, useReducedMotion } from 'motion/react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
@@ -50,7 +49,6 @@ import { fetchTokenKey, getApiKeys } from '@/features/keys/api'
import type { ApiKey } from '@/features/keys/types'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { getUserModels } from '@/lib/api'
import { MOTION_TRANSITION } from '@/lib/motion'
import { ROLE } from '@/lib/roles'
import { cn } from '@/lib/utils'
import { useAuthStore } from '@/stores/auth-store'
@@ -202,7 +200,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) {
'absolute right-3 [mask-image:linear-gradient(90deg,transparent_0%,black_30%,black_82%,transparent_100%)] text-right tracking-[0.38em] whitespace-pre',
props.compact
? '-top-6 text-[9px] leading-4'
: 'top-1 text-[11px] leading-5'
: 'top-1 text-xs leading-5'
)}
>
{SETUP_GUIDE_CODE_PATTERN}
@@ -254,7 +252,7 @@ function StartStepItem(props: {
</span>
<span className='flex min-w-0 flex-col gap-0.5'>
<span className='flex items-center gap-2 text-sm font-medium'>
<span className='text-muted-foreground font-mono text-xs tabular-nums'>
<span className='text-muted-foreground text-xs tabular-nums'>
{props.index + 1}.
</span>
<span className='truncate'>{props.step.title}</span>
@@ -278,7 +276,6 @@ function RequestPreview(props: {
signals: HeroSignal[]
}) {
const { t } = useTranslation()
const shouldReduceMotion = useReducedMotion()
const [isCopying, setIsCopying] = useState(false)
const { copyToClipboard } = useCopyToClipboard({ notify: false })
const previewCurl = buildCurlCommand({
@@ -316,21 +313,7 @@ function RequestPreview(props: {
}
return (
<motion.div
initial={shouldReduceMotion ? false : { opacity: 0, y: 10, scale: 0.98 }}
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0, scale: 1 }}
transition={MOTION_TRANSITION.slow}
className='bg-background/75 relative overflow-hidden rounded-2xl border p-3 shadow-sm backdrop-blur'
>
{!shouldReduceMotion && (
<motion.div
className='via-foreground/30 pointer-events-none absolute inset-x-0 top-0 h-px bg-linear-to-r from-transparent to-transparent'
animate={{ x: ['-100%', '100%'] }}
transition={{ duration: 3.2, repeat: Infinity, ease: 'easeInOut' }}
aria-hidden='true'
/>
)}
<div className='bg-background relative overflow-hidden rounded-2xl border p-3'>
<div className='flex items-center justify-between gap-3 border-b pb-3'>
<div className='flex min-w-0 items-center gap-2'>
<span className='bg-muted flex size-8 shrink-0 items-center justify-center rounded-lg'>
@@ -410,7 +393,7 @@ function RequestPreview(props: {
)
})}
</div>
</motion.div>
</div>
)
}
@@ -135,7 +135,7 @@ export function PerformanceHealthPanel() {
) : (
hasData && (
<div>
<span className='text-muted-foreground mb-1 block text-[11px] font-medium'>
<span className='text-muted-foreground mb-1 block text-xs font-medium'>
{t('Top models by traffic')}
</span>
<div className='grid grid-cols-1 gap-x-4 sm:grid-cols-2'>
@@ -144,7 +144,7 @@ export function PerformanceHealthPanel() {
key={model.model_name}
className='flex items-center justify-between gap-2 rounded px-1.5 py-1'
>
<span className='min-w-0 flex-1 truncate font-mono text-[11px]'>
<span className='min-w-0 flex-1 truncate font-mono text-xs'>
{model.model_name}
</span>
<span className='inline-flex shrink-0 items-center gap-1'>
@@ -157,7 +157,7 @@ export function PerformanceHealthPanel() {
/>
<span
className={cn(
'font-mono text-[11px] font-semibold tabular-nums',
'text-xs font-semibold tabular-nums',
getSuccessRateTextClass(model.success_rate)
)}
>
@@ -185,7 +185,7 @@ function MetricCell(props: {
const Icon = props.icon
return (
<div className='bg-muted/40 rounded-xl px-3 py-2.5'>
<div className='text-muted-foreground flex items-center gap-1.5 text-[11px] font-medium'>
<div className='text-muted-foreground flex items-center gap-1.5 text-xs font-medium'>
<Icon className='size-3 shrink-0' aria-hidden='true' />
<span className='truncate'>{props.label}</span>
</div>
@@ -194,7 +194,7 @@ function MetricCell(props: {
) : (
<div
className={cn(
'mt-1.5 font-mono text-sm font-semibold tabular-nums',
'mt-1.5 text-sm font-semibold tabular-nums',
props.valueClassName
)}
>
@@ -281,7 +281,7 @@ export function SummaryCards() {
className={cn('size-1.5 rounded-full', healthCfg.dotClass)}
aria-hidden='true'
/>
<span className='text-muted-foreground text-[11px] font-medium'>
<span className='text-muted-foreground text-xs font-medium'>
{t(healthCfg.labelKey)}
</span>
</span>
@@ -293,7 +293,7 @@ export function SummaryCards() {
<div className='grid grid-cols-2 gap-2'>
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
<div className='text-muted-foreground flex items-center gap-1 text-[11px] leading-none font-medium'>
<div className='text-muted-foreground flex items-center gap-1 text-xs leading-none font-medium'>
<Flame className='size-3 shrink-0' aria-hidden='true' />
<span className='truncate'>{t('Last 24h usage')}</span>
</div>
@@ -302,7 +302,7 @@ export function SummaryCards() {
</div>
</div>
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
<div className='text-muted-foreground flex items-center gap-1 text-[11px] leading-none font-medium'>
<div className='text-muted-foreground flex items-center gap-1 text-xs leading-none font-medium'>
{runwayDays !== null && runwayDays < 3 ? (
<TrendingDown
className='size-3 shrink-0'
@@ -32,10 +32,10 @@ import { cn } from '@/lib/utils'
import { PanelWrapper } from '../ui/panel-wrapper'
const STATUS_COLOR_MAP: Record<number, string> = {
1: 'bg-emerald-500',
0: 'bg-red-500',
2: 'bg-amber-500',
3: 'bg-blue-500',
1: 'bg-success',
0: 'bg-destructive',
2: 'bg-warning',
3: 'bg-info',
}
const DEFAULT_STATUS_COLOR = 'bg-muted-foreground/40'
@@ -131,7 +131,7 @@ export function UptimePanel() {
<h4 className='text-muted-foreground text-xs font-semibold tracking-wider uppercase'>
{group.categoryName}
</h4>
<span className='text-muted-foreground/40 font-mono text-xs tabular-nums'>
<span className='text-muted-foreground/40 text-xs tabular-nums'>
{group.monitors?.length || 0}
</span>
</div>
@@ -159,7 +159,7 @@ export function UptimePanel() {
</span>
)}
</div>
<span className='text-foreground shrink-0 font-mono text-sm font-semibold tabular-nums'>
<span className='text-foreground shrink-0 text-sm font-semibold tabular-nums'>
{((monitor.uptime ?? 0) * 100).toFixed(2)}%
</span>
</div>
@@ -42,7 +42,9 @@ function PanelHeader(props: {
}) {
const heading = (
<div className='flex flex-col gap-1'>
<div className='text-sm font-semibold'>{props.title}</div>
<div className='text-base font-semibold tracking-tight'>
{props.title}
</div>
{props.description != null && (
<div className='text-muted-foreground text-xs'>{props.description}</div>
)}
@@ -68,7 +70,7 @@ export function PanelWrapper(props: PanelWrapperProps) {
const resolvedEmptyMessage = props.emptyMessage ?? t('No data available')
const height = props.height ?? 'h-64'
const frameClassName = cn(
'overflow-hidden rounded-2xl border bg-card shadow-xs',
'overflow-hidden rounded-xl border bg-card',
props.className
)
@@ -51,15 +51,17 @@ interface StatCardProps {
action?: ReactNode
}
/* Sparklines are data visualization, so they draw from chart tokens — solid
* fills, no decorative gradients, consistent with the dashboard charts. */
const TONE_CLASSES: Record<StatCardTone, string> = {
rose: 'from-rose-500/80 via-rose-300/70 to-rose-200/20 dark:from-rose-400/70 dark:via-rose-500/30 dark:to-rose-500/5',
teal: 'from-teal-500/80 via-teal-300/70 to-teal-200/20 dark:from-teal-400/70 dark:via-teal-500/30 dark:to-teal-500/5',
gray: 'from-muted-foreground/50 via-muted-foreground/20 to-transparent dark:from-muted-foreground/40 dark:via-muted-foreground/20',
rose: 'bg-chart-5/60',
teal: 'bg-chart-2/60',
gray: 'bg-muted-foreground/30',
}
const LINE_TONE_CLASSES: Record<StatCardTone, string> = {
rose: 'text-warning',
teal: 'text-primary',
rose: 'text-chart-5',
teal: 'text-chart-2',
gray: 'text-muted-foreground',
}
@@ -166,7 +168,7 @@ function BarSparkline(props: { values?: number[]; tone: StatCardTone }) {
<span
key={`spark-${index}`}
className={cn(
'flex-1 rounded-t-sm bg-linear-to-t',
'flex-1 rounded-t-sm',
height <= 0 && 'opacity-20',
TONE_CLASSES[props.tone]
)}
@@ -185,7 +187,7 @@ function StatCardDetails(props: { details: StatCardDetail[] }) {
key={detail.label}
className='bg-muted/40 rounded-lg border border-transparent px-2.5 py-2'
>
<div className='text-muted-foreground truncate text-[11px] leading-none font-medium'>
<div className='text-muted-foreground truncate text-xs leading-none font-medium'>
{detail.label}
</div>
<div
@@ -228,7 +230,7 @@ export function StatCard(props: StatCardProps) {
</div>
) : props.error ? (
<div className='flex flex-col gap-1'>
<div className='text-muted-foreground mt-0.5 font-mono text-base font-bold tracking-tight break-all tabular-nums sm:text-2xl'>
<div className='text-muted-foreground mt-0.5 text-2xl font-semibold tracking-tight break-all tabular-nums'>
--
</div>
<p className='text-muted-foreground/60 text-xs'>
@@ -237,7 +239,7 @@ export function StatCard(props: StatCardProps) {
</div>
) : (
<div className='flex flex-col gap-1'>
<div className='text-foreground font-mono text-2xl font-semibold tracking-tight break-all tabular-nums'>
<div className='text-foreground text-2xl font-semibold tracking-tight break-all tabular-nums'>
{props.value}
</div>
<p className='text-muted-foreground/60 text-xs leading-relaxed'>
@@ -62,19 +62,19 @@ function formatGroupRatio(
function getRatioBadgeClassName(ratio: ApiKeyGroupOption['ratio']) {
if (typeof ratio !== 'number') {
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-300'
return 'border-success/25 bg-success/10 text-success'
}
if (ratio > 5) {
return 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-300'
return 'border-destructive/25 bg-destructive/10 text-destructive'
}
if (ratio > 3) {
return 'border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-900/60 dark:bg-orange-950/40 dark:text-orange-300'
return 'border-warning/30 bg-warning/10 text-warning'
}
if (ratio > 1) {
return 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-900/60 dark:bg-blue-950/40 dark:text-blue-300'
return 'border-info/25 bg-info/10 text-info'
}
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-300'
return 'border-success/25 bg-success/10 text-success'
}
function GroupRatioBadge({ ratio }: { ratio: ApiKeyGroupOption['ratio'] }) {
@@ -87,7 +87,7 @@ function GroupRatioBadge({ ratio }: { ratio: ApiKeyGroupOption['ratio'] }) {
<Badge
variant='outline'
className={cn(
'max-w-24 shrink-0 truncate text-[10px] sm:max-w-none sm:text-xs',
'max-w-24 shrink-0 truncate text-xs sm:max-w-none sm:text-xs',
getRatioBadgeClassName(ratio)
)}
>
@@ -149,7 +149,7 @@ export function ApiKeyGroupCombobox({
{selectedOption?.label || placeholder || t('Select a group')}
</span>
{selectedOption?.desc && (
<span className='text-muted-foreground block truncate text-[11px] sm:text-xs'>
<span className='text-muted-foreground block truncate text-xs sm:text-xs'>
{selectedOption.desc}
</span>
)}
@@ -44,9 +44,10 @@ import {
import { DataTableRowActions } from './data-table-row-actions'
function getQuotaProgressColor(percentage: number): string {
if (percentage <= 10) return '[&_[data-slot=progress-indicator]]:bg-rose-500'
if (percentage <= 30) return '[&_[data-slot=progress-indicator]]:bg-amber-500'
return '[&_[data-slot=progress-indicator]]:bg-emerald-500'
if (percentage <= 10)
return '[&_[data-slot=progress-indicator]]:bg-destructive'
if (percentage <= 30) return '[&_[data-slot=progress-indicator]]:bg-warning'
return '[&_[data-slot=progress-indicator]]:bg-success'
}
function useGroupRatios(): Record<string, number> {
@@ -254,7 +255,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
accessorKey: 'created_time',
header: t('Created'),
cell: ({ row }) => (
<span className='text-muted-foreground block truncate font-mono text-xs tabular-nums'>
<span className='text-muted-foreground block truncate text-xs tabular-nums'>
{formatTimestampToDate(row.getValue('created_time'))}
</span>
),
@@ -270,7 +271,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
return <span className='text-muted-foreground text-xs'>-</span>
}
return (
<span className='text-muted-foreground block truncate font-mono text-xs tabular-nums'>
<span className='text-muted-foreground block truncate text-xs tabular-nums'>
{formatTimestampToDate(accessedTime)}
</span>
)
@@ -296,7 +297,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
return (
<span
className={cn(
'block truncate font-mono text-xs tabular-nums',
'block truncate text-xs tabular-nums',
isExpired ? 'text-destructive' : 'text-muted-foreground'
)}
>
@@ -140,7 +140,7 @@ function ApiKeysMobileList({
<div className='truncate text-sm font-semibold'>
{apiKey.name}
</div>
<div className='text-muted-foreground text-[11px]'>
<div className='text-muted-foreground text-xs'>
{t('API Key')}
</div>
</div>
@@ -203,7 +203,7 @@ export function DataTableRowActions<TData>({
className={
isEnabled
? 'text-destructive hover:text-destructive'
: 'text-emerald-600 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-400'
: 'text-success hover:text-success'
}
/>
}
+1 -5
View File
@@ -125,11 +125,7 @@ export function LegalDocument({
return (
<PublicLayout showMainContainer={!contentIsHtml}>
{contentIsHtml ? (
<RichContent
mode='html'
htmlVariant='isolated'
content={rawContent}
/>
<RichContent mode='html' htmlVariant='isolated' content={rawContent} />
) : (
<div className='mx-auto max-w-4xl space-y-6 py-12'>
<div className='space-y-2'>
@@ -141,8 +141,8 @@ export function DeploymentAccessGuard({
return (
<div className='mx-auto mt-8 max-w-md'>
<div className='text-center'>
<div className='mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-amber-100 dark:bg-amber-900/20'>
<Server className='h-8 w-8 text-amber-600 dark:text-amber-400' />
<div className='bg-warning/10 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl'>
<Server className='text-warning h-8 w-8' />
</div>
<h3 className='mb-6 text-xl font-semibold'>
{t('Model deployment service is disabled')}
+8 -8
View File
@@ -61,18 +61,18 @@ export function getSuccessRateLevel(rate: number): SuccessRateLevel {
}
const SUCCESS_RATE_TEXT_CLASS: Record<SuccessRateLevel, string> = {
excellent: 'text-emerald-600 dark:text-emerald-400',
good: 'text-emerald-500 dark:text-emerald-300',
warning: 'text-amber-600 dark:text-amber-400',
critical: 'text-red-600 dark:text-red-400',
excellent: 'text-success',
good: 'text-success/80',
warning: 'text-warning',
critical: 'text-destructive',
unknown: 'text-muted-foreground',
}
const SUCCESS_RATE_DOT_CLASS: Record<SuccessRateLevel, string> = {
excellent: 'bg-emerald-500',
good: 'bg-emerald-400',
warning: 'bg-amber-500',
critical: 'bg-red-500',
excellent: 'bg-success',
good: 'bg-success/80',
warning: 'bg-warning',
critical: 'bg-destructive',
unknown: 'bg-muted-foreground',
}
@@ -124,7 +124,7 @@ function PlaygroundParameterContent({
{t(control.labelKey)}
</label>
<Badge
className='h-5 max-w-24 shrink-0 px-1.5 font-mono text-[11px]'
className='h-5 max-w-24 shrink-0 px-1.5 font-mono text-xs'
variant='outline'
>
{t(getParameterControlValueText(control.key, value))}
@@ -68,7 +68,7 @@ export function MessageMetadata(props: MessageMetadataProps) {
return (
<div
className={cn(
'text-muted-foreground mt-1 flex min-h-4 items-center gap-1.5 text-[11px] leading-none',
'text-muted-foreground mt-1 flex min-h-4 items-center gap-1.5 text-xs leading-none',
props.alignment === 'right' && 'justify-end'
)}
>
@@ -199,7 +199,7 @@ export function DynamicPricingBreakdown({
<section className={cn('min-w-0', !compact && 'py-4')}>
{!compact && (
<div className='mb-3 flex items-center gap-2'>
<span className='inline-flex size-6 items-center justify-center rounded-lg bg-amber-100 text-amber-700 shadow-sm dark:bg-amber-500/20 dark:text-amber-300'>
<span className='bg-warning/15 text-warning inline-flex size-6 items-center justify-center rounded-lg'>
<TagIcon className='size-3.5' />
</span>
<div>
@@ -212,7 +212,7 @@ export function DynamicPricingBreakdown({
</div>
</div>
)}
<div className='text-muted-foreground mb-1 text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground mb-1 text-xs font-medium tracking-wider uppercase'>
{t('Raw expression')}
</div>
<code className='text-muted-foreground block text-xs break-all'>
@@ -234,7 +234,7 @@ export function DynamicPricingBreakdown({
<section className={cn('min-w-0', !compact && 'py-3 sm:py-4')}>
{!compact && (
<div className='mb-3 flex items-start gap-2 sm:mb-4'>
<span className='mt-0.5 inline-flex size-6 items-center justify-center rounded-lg bg-amber-100 text-amber-700 shadow-sm dark:bg-amber-500/20 dark:text-amber-300'>
<span className='bg-warning/15 text-warning mt-0.5 inline-flex size-6 items-center justify-center rounded-lg'>
<TagIcon className='size-3.5' />
</span>
<div>
@@ -271,7 +271,7 @@ export function DynamicPricingBreakdown({
key={`tier-mobile-${i}`}
className={cn(
'rounded-md border p-2',
isMatched && 'border-emerald-500/40 bg-emerald-500/10'
isMatched && 'border-success/40 bg-success/10'
)}
>
<div className='mb-1.5 flex flex-wrap items-center gap-1.5'>
@@ -284,7 +284,7 @@ export function DynamicPricingBreakdown({
{isMatched && (
<Badge
variant='secondary'
className='bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
className='bg-success/10 text-success'
>
{t('Matched')}
</Badge>
@@ -302,7 +302,7 @@ export function DynamicPricingBreakdown({
)
return (
<div key={v.field} className='min-w-0'>
<div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
{t(v.shortLabel)}
</div>
<div
@@ -337,10 +337,7 @@ export function DynamicPricingBreakdown({
const isMatched =
normalizedMatchedTierLabel !== '' &&
normalizeTierLabel(tier.label) === normalizedMatchedTierLabel
return cn(
isMatched &&
'bg-emerald-50/70 hover:bg-emerald-50/70 dark:bg-emerald-500/10 dark:hover:bg-emerald-500/10'
)
return cn(isMatched && 'bg-success/10 hover:bg-success/10')
}}
columns={[
{
@@ -369,7 +366,7 @@ export function DynamicPricingBreakdown({
{isMatched && (
<Badge
variant='secondary'
className='bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
className='bg-success/10 text-success'
>
{t('Matched')}
</Badge>
+2 -2
View File
@@ -96,10 +96,10 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
if (dynamicSummary.isSpecialExpression) {
priceSummary = (
<span className='min-w-0'>
<span className='text-amber-700 dark:text-amber-300'>
<span className='text-warning'>
{t('Special billing expression')}
</span>
<code className='text-muted-foreground/70 mt-0.5 line-clamp-1 block font-mono text-[11px] break-all'>
<code className='text-muted-foreground/70 mt-0.5 line-clamp-1 block font-mono text-xs break-all'>
{dynamicSummary.rawExpression}
</code>
</span>
@@ -539,7 +539,7 @@ function CodeSamplesSection(props: {
<p className='text-muted-foreground mt-2 text-xs'>
{t('Replace')}{' '}
<code className='bg-muted rounded px-1 py-0.5 font-mono text-[11px]'>
<code className='bg-muted rounded px-1 py-0.5 font-mono text-xs'>
{'<YOUR_API_KEY>'}
</code>{' '}
{t('with the API key from your token settings.')}
@@ -582,7 +582,7 @@ function SupportedParametersSection(props: { model: PricingModel }) {
{p.required && (
<Badge
variant='outline'
className='h-6 border-rose-500/40 px-2 text-sm text-rose-600 dark:text-rose-400'
className='border-destructive/40 text-destructive h-6 px-2 text-sm'
>
{t('required')}
</Badge>
@@ -711,7 +711,7 @@ function RateLimitsSection(props: { model: PricingModel }) {
},
]}
/>
<p className='text-muted-foreground mt-2 text-[11px] leading-relaxed'>
<p className='text-muted-foreground mt-2 text-xs leading-relaxed'>
{t(
'RPM = requests per minute, TPM = tokens per minute, RPD = requests per day. Limits apply per token group.'
)}
@@ -734,11 +734,11 @@ function AuthSection() {
<div className='space-y-1.5 text-xs leading-relaxed'>
<p>
{t('All requests must include')}{' '}
<code className='bg-muted rounded px-1 py-0.5 font-mono text-[11px]'>
<code className='bg-muted rounded px-1 py-0.5 font-mono text-xs'>
Authorization: Bearer &lt;TOKEN&gt;
</code>{' '}
{t('header. Anthropic-formatted endpoints accept the')}{' '}
<code className='bg-muted rounded px-1 py-0.5 font-mono text-[11px]'>
<code className='bg-muted rounded px-1 py-0.5 font-mono text-xs'>
x-api-key
</code>{' '}
{t('header instead.')}
@@ -48,16 +48,14 @@ function RankBadge(props: { rank: number }) {
const isPodium = rank <= 3
const palette =
rank === 1
? 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'
: rank === 2
? 'bg-slate-100 text-slate-700 dark:bg-slate-500/20 dark:text-slate-300'
: rank === 3
? 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300'
: 'bg-muted text-muted-foreground'
? 'bg-warning/15 text-warning'
: rank <= 3
? 'bg-muted text-foreground'
: 'bg-muted text-muted-foreground'
return (
<span
className={cn(
'inline-flex size-7 shrink-0 items-center justify-center rounded-md font-mono text-xs font-bold tabular-nums',
'inline-flex size-7 shrink-0 items-center justify-center rounded-md text-xs font-bold tabular-nums',
palette
)}
>
@@ -71,16 +69,16 @@ function GrowthChip(props: { value: number }) {
const isUp = value > 0
const isDown = value < 0
const palette = isUp
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
? 'bg-success/10 text-success'
: isDown
? 'bg-rose-100 text-rose-700 dark:bg-rose-500/20 dark:text-rose-300'
? 'bg-destructive/10 text-destructive'
: 'bg-muted text-muted-foreground'
const Icon = isUp ? ArrowUpRight : isDown ? ArrowDownRight : null
const formatted = `${value > 0 ? '+' : ''}${value.toFixed(1)}%`
return (
<span
className={cn(
'inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 font-mono text-[11px] font-semibold tabular-nums',
'inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 text-xs font-semibold tabular-nums',
palette
)}
>
@@ -125,35 +123,35 @@ export function ModelDetailsApps(props: { model: PricingModel }) {
<div className='flex flex-col gap-4'>
<div className='grid grid-cols-1 gap-2 sm:grid-cols-3'>
<div className='bg-muted/20 rounded-lg border p-3'>
<div className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
{t('Tracked apps')}
</div>
<div className='text-foreground mt-1 font-mono text-lg font-semibold tabular-nums'>
<div className='text-foreground mt-1 text-lg font-semibold tabular-nums'>
{apps.length}
</div>
<p className='text-muted-foreground/70 text-[11px]'>
<p className='text-muted-foreground/70 text-xs'>
{t('Top integrations using this model')}
</p>
</div>
<div className='bg-muted/20 rounded-lg border p-3'>
<div className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
{t('Monthly tokens')}
</div>
<div className='text-foreground mt-1 font-mono text-lg font-semibold tabular-nums'>
<div className='text-foreground mt-1 text-lg font-semibold tabular-nums'>
{COMPACT_NUMBER.format(totalMonthlyTokens)}
</div>
<p className='text-muted-foreground/70 text-[11px]'>
<p className='text-muted-foreground/70 text-xs'>
{t('Aggregated across the apps below')}
</p>
</div>
<div className='bg-muted/20 rounded-lg border p-3'>
<div className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
{t('#1 by usage')}
</div>
<div className='text-foreground mt-1 truncate text-base font-semibold'>
{top.name}
</div>
<p className='text-muted-foreground/70 truncate text-[11px]'>
<p className='text-muted-foreground/70 truncate text-xs'>
{top.category} · {formatTokenVolume(top.monthly_tokens)}{' '}
{t('tokens / mo')}
</p>
@@ -225,7 +223,7 @@ export function ModelDetailsApps(props: { model: PricingModel }) {
]}
/>
<p className='text-muted-foreground/60 text-[11px] leading-relaxed'>
<p className='text-muted-foreground/60 text-xs leading-relaxed'>
{t(
'App rankings shown here are simulated for preview purposes and will be replaced with live usage data once the backend integration is complete.'
)}
@@ -51,22 +51,20 @@ function StatCard(props: {
const Icon = props.icon
return (
<div className='bg-background flex flex-col gap-1 rounded-lg border p-3'>
<span className='text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider uppercase'>
<span className='text-muted-foreground inline-flex items-center gap-1.5 text-xs font-medium tracking-wider uppercase'>
<Icon className='size-3' />
{props.label}
</span>
<span
className={cn(
'text-foreground font-mono text-lg font-semibold tabular-nums',
'text-foreground text-lg font-semibold tabular-nums',
props.valueClassName
)}
>
{props.value}
</span>
{props.hint && (
<span className='text-muted-foreground/70 text-[11px]'>
{props.hint}
</span>
<span className='text-muted-foreground/70 text-xs'>{props.hint}</span>
)}
</div>
)
@@ -330,7 +328,7 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
}
accent={
incidentCount > 0 ? (
<span className='inline-flex items-center gap-1 text-amber-600 dark:text-amber-400'>
<span className='text-warning inline-flex items-center gap-1'>
<AlertTriangle className='size-3.5' />
{t('{{count}} incidents', {
count: incidentCount,
@@ -128,7 +128,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
{showOverall && (
<span
className={cn(
'font-mono text-sm font-semibold tabular-nums',
'text-sm font-semibold tabular-nums',
getSuccessRateTextClass(overall)
)}
>
@@ -164,13 +164,11 @@ export function UptimeStatusRow(props: {
: AlertCircle
const statusColour =
status === 'operational'
? 'text-emerald-600 dark:text-emerald-400'
: status === 'minor'
? 'text-emerald-600 dark:text-emerald-400'
: status === 'degraded'
? 'text-amber-600 dark:text-amber-400'
: 'text-rose-600 dark:text-rose-400'
status === 'operational' || status === 'minor'
? 'text-success'
: status === 'degraded'
? 'text-warning'
: 'text-destructive'
const statusLabel =
status === 'operational'
+20 -20
View File
@@ -156,12 +156,12 @@ function OverviewMetric(props: {
<div className='flex min-w-0 items-center gap-2 px-3 py-2'>
<Icon className='text-muted-foreground/70 size-3.5 shrink-0' />
<div className='min-w-0 flex-1'>
<div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
{props.label}
</div>
<div
className={cn(
'text-foreground truncate font-mono text-sm font-semibold tabular-nums',
'text-foreground truncate text-sm font-semibold tabular-nums',
props.valueClassName
)}
>
@@ -254,7 +254,7 @@ function CatalogTextValue(props: { children: React.ReactNode }) {
function CatalogInfoCell(props: { label: string; children: React.ReactNode }) {
return (
<div className='bg-card flex min-w-0 flex-col gap-1 px-3 py-2.5'>
<span className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
<span className='text-muted-foreground text-xs font-medium tracking-wider uppercase'>
{props.label}
</span>
{props.children}
@@ -361,7 +361,7 @@ function ModelBackendQuickStats(props: { model: PricingModel }) {
key={stat.key}
className='bg-background flex min-w-0 flex-col gap-0.5 px-3 py-2.5'
>
<span className='text-muted-foreground inline-flex min-w-0 items-center gap-1 text-[10px] font-medium tracking-wider uppercase'>
<span className='text-muted-foreground inline-flex min-w-0 items-center gap-1 text-xs font-medium tracking-wider uppercase'>
<Icon className='size-3 shrink-0' />
<span className='truncate'>{stat.label}</span>
</span>
@@ -369,7 +369,7 @@ function ModelBackendQuickStats(props: { model: PricingModel }) {
{stat.value}
</span>
{stat.hint && (
<span className='text-muted-foreground/60 truncate text-[10px]'>
<span className='text-muted-foreground/60 truncate text-xs'>
{stat.hint}
</span>
)}
@@ -565,7 +565,7 @@ function ModelHeader(props: { model: PricingModel }) {
{model.billing_mode === 'tiered_expr' && model.billing_expr && (
<>
<span className='text-muted-foreground/30'>·</span>
<span className='rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'>
<span className='bg-warning/15 text-warning rounded px-1.5 py-0.5 text-xs font-medium'>
{isSpecialExpression
? t('Special billing expression')
: t('Dynamic Pricing')}
@@ -649,15 +649,15 @@ function PriceSection(props: {
return (
<section>
<SectionTitle>{t('Base Price')}</SectionTitle>
<div className='rounded-lg border border-amber-200/70 bg-amber-50/70 p-3 dark:border-amber-500/20 dark:bg-amber-500/10'>
<div className='text-sm font-medium text-amber-800 dark:text-amber-200'>
<div className='border-warning/30 bg-warning/10 rounded-lg border p-3'>
<div className='text-warning text-sm font-medium'>
{t('Special billing expression')}
</div>
<p className='text-muted-foreground mt-1 text-xs'>
{t('Unable to parse structured pricing')}
</p>
<div className='mt-3'>
<div className='text-muted-foreground mb-1 text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground mb-1 text-xs font-medium tracking-wider uppercase'>
{t('Raw expression')}
</div>
<code className='text-muted-foreground bg-background/80 block max-h-28 overflow-auto rounded-md border px-2 py-1.5 font-mono text-xs break-all'>
@@ -682,7 +682,7 @@ function PriceSection(props: {
<div className='text-muted-foreground text-xs'>
{t(entry.shortLabel)}
</div>
<div className='text-foreground mt-1 font-mono text-base font-semibold tabular-nums'>
<div className='text-foreground mt-1 text-base font-semibold tabular-nums'>
{entry.formatted}
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
/ {tokenUnitLabel}
@@ -707,7 +707,7 @@ function PriceSection(props: {
<span className='text-muted-foreground/70 text-sm'>
{t(entry.shortLabel)}
</span>
<span className='text-muted-foreground font-mono text-sm tabular-nums'>
<span className='text-muted-foreground text-sm tabular-nums'>
{entry.formatted}
<span className='text-muted-foreground/40 ml-1 text-xs font-normal'>
/ {tokenUnitLabel}
@@ -730,7 +730,7 @@ function PriceSection(props: {
<span className='text-muted-foreground text-sm'>
{t('Per request')}
</span>
<span className='text-foreground font-mono text-sm font-semibold tabular-nums'>
<span className='text-foreground text-sm font-semibold tabular-nums'>
{formatFixedPrice(
props.model,
baseGroupKey,
@@ -771,7 +771,7 @@ function PriceSection(props: {
{primaryPriceTypes.map((item) => (
<div key={item.type} className='bg-muted/20 rounded-lg border p-3'>
<div className='text-muted-foreground text-xs'>{item.label}</div>
<div className='text-foreground mt-1 font-mono text-base font-semibold tabular-nums'>
<div className='text-foreground mt-1 text-base font-semibold tabular-nums'>
{renderPrice(item.type)}
</div>
</div>
@@ -788,7 +788,7 @@ function PriceSection(props: {
<span className='text-muted-foreground/70 text-sm'>
{item.label}
</span>
<span className='text-muted-foreground font-mono text-sm tabular-nums'>
<span className='text-muted-foreground text-sm tabular-nums'>
{renderPrice(item.type)}
</span>
</div>
@@ -923,7 +923,7 @@ function GroupPricingSection(props: {
}
const thClass =
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase'
'text-muted-foreground py-2 text-xs font-medium tracking-wider uppercase'
if (isDynamicPricingModel(props.model)) {
const dynamicTiers = getDynamicPricingTiers(props.model)
@@ -933,8 +933,8 @@ function GroupPricingSection(props: {
<section>
<SectionTitle>{t('Pricing by Group')}</SectionTitle>
<AutoGroupChain model={props.model} autoGroups={props.autoGroups} />
<div className='rounded-lg border border-amber-200/70 bg-amber-50/70 p-3 dark:border-amber-500/20 dark:bg-amber-500/10'>
<div className='text-sm font-medium text-amber-800 dark:text-amber-200'>
<div className='border-warning/30 bg-warning/10 rounded-lg border p-3'>
<div className='text-warning text-sm font-medium'>
{t('Special billing expression')}
</div>
<p className='text-muted-foreground mt-1 text-xs'>
@@ -943,7 +943,7 @@ function GroupPricingSection(props: {
)}
</p>
<div className='mt-3'>
<div className='text-muted-foreground mb-1 text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground mb-1 text-xs font-medium tracking-wider uppercase'>
{t('Raw expression')}
</div>
<code className='text-muted-foreground bg-background/80 block max-h-28 overflow-auto rounded-md border px-2 py-1.5 font-mono text-xs break-all'>
@@ -1028,7 +1028,7 @@ function GroupPricingSection(props: {
</div>
)
})}
<p className='text-muted-foreground/40 mt-1.5 text-[10px]'>
<p className='text-muted-foreground/40 mt-1.5 text-xs'>
{t('Prices shown per')} {tokenUnitLabel} tokens
</p>
</div>
@@ -1119,7 +1119,7 @@ function GroupPricingSection(props: {
/>
<div className='-mx-4 sm:mx-0'>
{isTokenBased && (
<p className='text-muted-foreground/40 mt-1.5 px-4 text-[10px] sm:px-0'>
<p className='text-muted-foreground/40 mt-1.5 px-4 text-xs sm:px-0'>
{t('Prices shown per')} {tokenUnitLabel} tokens
</p>
)}
@@ -79,7 +79,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
)}
>
<div title={t('Average latency')} className='min-w-0'>
<div className='text-muted-foreground/55 text-[10px] leading-4'>
<div className='text-muted-foreground/55 text-xs leading-4'>
{t('Latency short')}
</div>
<div className='text-muted-foreground/80 font-mono text-xs leading-4 whitespace-nowrap'>
@@ -87,7 +87,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
</div>
</div>
<div title={t('Throughput')} className='min-w-0'>
<div className='text-muted-foreground/55 truncate text-[10px] leading-4'>
<div className='text-muted-foreground/55 truncate text-xs leading-4'>
{t('Throughput short')}
</div>
<div className='text-muted-foreground/80 font-mono text-xs leading-4 whitespace-nowrap'>
@@ -98,7 +98,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
title={`${t('Success rate')}: ${success_rate.toFixed(1)}%`}
className='min-w-0'
>
<div className='text-muted-foreground/55 truncate text-[10px] leading-4'>
<div className='text-muted-foreground/55 truncate text-xs leading-4'>
{t('Status short')}
</div>
<div className='flex h-4 items-center justify-end gap-0.5'>
@@ -135,13 +135,13 @@ export function usePricingColumns(
if (dynamicSummary.isSpecialExpression) {
return (
<div className='max-w-full min-w-0'>
<div className='text-xs font-medium text-amber-700 dark:text-amber-300'>
<div className='text-warning text-xs font-medium'>
{t('Special billing expression')}
</div>
<div className='text-muted-foreground text-[11px]'>
<div className='text-muted-foreground text-xs'>
{t('Unable to parse structured pricing')}
</div>
<code className='text-muted-foreground/70 mt-1 line-clamp-2 block font-mono text-[10px] leading-relaxed break-all'>
<code className='text-muted-foreground/70 mt-1 line-clamp-2 block font-mono text-xs leading-relaxed break-all'>
{dynamicSummary.rawExpression}
</code>
</div>
@@ -159,7 +159,7 @@ export function usePricingColumns(
return (
<div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>
<span className='text-sm tabular-nums'>
{primaryEntries.map((entry, index) => (
<span key={entry.key}>
{index > 0 && (
@@ -169,7 +169,7 @@ export function usePricingColumns(
</span>
))}
</span>
<div className='text-muted-foreground/50 text-[10px]'>
<div className='text-muted-foreground/50 text-xs'>
/ {tokenUnitLabel} tokens
{dynamicSummary.tierCount > 1 &&
` · ${t('{{count}} tiers', {
@@ -208,12 +208,12 @@ export function usePricingColumns(
return (
<div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>
<span className='text-sm tabular-nums'>
{inputPrice}
<span className='text-muted-foreground/40 mx-1'>/</span>
{outputPrice}
</span>
<div className='text-muted-foreground/50 text-[10px]'>
<div className='text-muted-foreground/50 text-xs'>
/ {tokenUnitLabel} tokens
</div>
</div>
@@ -232,8 +232,8 @@ export function usePricingColumns(
return (
<div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>{price}</span>
<div className='text-muted-foreground/50 text-[10px]'>
<span className='text-sm tabular-nums'>{price}</span>
<div className='text-muted-foreground/50 text-xs'>
/ {t('request')}
</div>
</div>
@@ -278,10 +278,10 @@ export function usePricingColumns(
return (
<div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>
<span className='text-sm tabular-nums'>
{stripTrailingZeros(cacheEntry.formatted)}
</span>
<div className='text-muted-foreground/50 text-[10px]'>
<div className='text-muted-foreground/50 text-xs'>
/ {tokenUnitLabel}
</div>
</div>
@@ -308,10 +308,8 @@ export function usePricingColumns(
return (
<div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>
{cachedPrice}
</span>
<div className='text-muted-foreground/50 text-[10px]'>
<span className='text-sm tabular-nums'>{cachedPrice}</span>
<div className='text-muted-foreground/50 text-xs'>
/ {tokenUnitLabel}
</div>
</div>
@@ -115,7 +115,7 @@ function FilterChip(props: {
{(props.option.suffix || props.option.count != null) && (
<span
className={cn(
'rounded-md px-1.5 py-0.5 text-[10px]',
'rounded-md px-1.5 py-0.5 text-xs',
props.active
? 'bg-background text-foreground'
: 'bg-muted text-muted-foreground'
@@ -179,7 +179,7 @@ export function PricingToolbar(props: PricingToolbarProps) {
<Filter className='size-4' />
{t('Filter')}
{props.activeFilterCount > 0 && (
<Badge className='ml-0.5 size-5 justify-center p-0 text-[10px]'>
<Badge className='ml-0.5 size-5 justify-center p-0 text-xs'>
{props.activeFilterCount}
</Badge>
)}
+1 -1
View File
@@ -78,7 +78,7 @@ export function SearchBar(props: SearchBarProps) {
<X className='size-4' />
</Button>
) : (
<kbd className='bg-muted text-muted-foreground pointer-events-none hidden rounded border px-1.5 py-0.5 font-mono text-[10px] sm:inline-block'>
<kbd className='bg-muted text-muted-foreground pointer-events-none hidden rounded border px-1.5 py-0.5 font-mono text-xs sm:inline-block'>
K
</kbd>
)}
@@ -294,7 +294,7 @@ export function CheckinCalendarCard({
{t('Daily Check-in')}
</h3>
{checkedToday && (
<div className='inline-flex items-center gap-1 rounded-md bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-600 sm:gap-1.5 sm:px-2.5 sm:text-xs dark:text-emerald-400'>
<div className='bg-success/10 text-success inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium sm:gap-1.5 sm:px-2.5'>
<Sparkles className='h-2.5 w-2.5 sm:h-3 sm:w-3' />
{t('Checked in')}
</div>
@@ -337,7 +337,7 @@ export function CheckinCalendarCard({
<div className='text-xl font-semibold tracking-tight tabular-nums sm:text-2xl'>
{checkinData?.stats?.total_checkins || 0}
</div>
<div className='text-muted-foreground mt-0.5 text-[10px] font-medium sm:mt-1 sm:text-xs'>
<div className='text-muted-foreground mt-0.5 text-xs font-medium sm:mt-1 sm:text-xs'>
{t('Total check-ins')}
</div>
</div>
@@ -345,7 +345,7 @@ export function CheckinCalendarCard({
<div className='text-xl font-semibold tracking-tight tabular-nums sm:text-2xl'>
{formatQuotaWithCurrency(monthlyQuota, { digitsLarge: 0 })}
</div>
<div className='text-muted-foreground mt-0.5 text-[10px] font-medium sm:mt-1 sm:text-xs'>
<div className='text-muted-foreground mt-0.5 text-xs font-medium sm:mt-1 sm:text-xs'>
{t('This month')}
</div>
</div>
@@ -358,7 +358,7 @@ export function CheckinCalendarCard({
}
)}
</div>
<div className='text-muted-foreground mt-0.5 text-[10px] font-medium sm:mt-1 sm:text-xs'>
<div className='text-muted-foreground mt-0.5 text-xs font-medium sm:mt-1 sm:text-xs'>
{t('Total earned')}
</div>
</div>
@@ -398,7 +398,7 @@ export function CheckinCalendarCard({
{weekDays.map((day) => (
<div
key={day}
className='text-muted-foreground flex h-7 items-center justify-center text-[10px] font-medium sm:h-8 sm:text-xs'
className='text-muted-foreground flex h-7 items-center justify-center text-xs font-medium sm:h-8 sm:text-xs'
>
{day}
</div>
@@ -430,7 +430,7 @@ export function CheckinCalendarCard({
>
<span className='tabular-nums'>{dayNum}</span>
{isCheckedIn && !isToday && (
<span className='absolute bottom-0.5 h-1 w-1 rounded-full bg-emerald-500 sm:bottom-1' />
<span className='bg-success absolute bottom-0.5 h-1 w-1 rounded-full sm:bottom-1' />
)}
</Button>
)
@@ -458,7 +458,7 @@ export function CheckinCalendarCard({
</div>
{/* Footer hint */}
<div className='text-muted-foreground border-t pt-3 text-center text-[11px] sm:pt-4 sm:text-xs'>
<div className='text-muted-foreground border-t pt-3 text-center text-xs sm:pt-4 sm:text-xs'>
{t('You can only check in once per day')}
</div>
@@ -335,7 +335,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
{showUnsupportedNotice && (
<div className='bg-muted/60 text-muted-foreground flex items-start gap-3 rounded-md p-4 text-sm'>
<ShieldAlert className='mt-0.5 h-4 w-4 flex-shrink-0 text-amber-500' />
<ShieldAlert className='text-warning mt-0.5 h-4 w-4 flex-shrink-0' />
<div>
<p className='text-foreground font-medium'>
{t('Passkey not supported on this device')}
@@ -163,7 +163,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
</div>
</div>
<div className='text-foreground mt-1.5 truncate font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
<div className='text-foreground mt-1.5 truncate text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
{item.value}
</div>
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
@@ -33,10 +33,7 @@ export function GrowthText(props: GrowthTextProps) {
if (!Number.isFinite(v) || v === 0) {
return (
<span
className={cn(
'text-muted-foreground/80 font-mono tabular-nums',
props.className
)}
className={cn('text-muted-foreground/80 tabular-nums', props.className)}
>
0%
</span>
@@ -46,10 +43,8 @@ export function GrowthText(props: GrowthTextProps) {
return (
<span
className={cn(
'font-mono tabular-nums',
isUp
? 'text-emerald-600 dark:text-emerald-400'
: 'text-rose-600 dark:text-rose-400',
'tabular-nums',
isUp ? 'text-success' : 'text-destructive',
props.className
)}
>
@@ -279,7 +279,7 @@ function VendorList(props: {
<ul>
{props.rows.map((vendor) => (
<li key={vendor.vendor} className='flex items-center gap-3 py-2.5'>
<span className='text-muted-foreground/80 w-6 shrink-0 text-right font-mono text-xs tabular-nums'>
<span className='text-muted-foreground/80 w-6 shrink-0 text-right text-xs tabular-nums'>
{vendor.rank}.
</span>
<span
@@ -296,10 +296,10 @@ function VendorList(props: {
{vendor.vendor}
</VendorLink>
<div className='shrink-0 text-right'>
<div className='text-foreground font-mono text-sm font-semibold tabular-nums'>
<div className='text-foreground text-sm font-semibold tabular-nums'>
{formatTokens(vendor.total_tokens)}
</div>
<div className='text-muted-foreground/80 font-mono text-[11px] tabular-nums'>
<div className='text-muted-foreground/80 text-xs tabular-nums'>
{formatShare(vendor.share)}
</div>
</div>
@@ -79,7 +79,7 @@ function ModelList(props: {
: 'flex items-center gap-3 py-2.5'
}
>
<span className='text-muted-foreground/80 w-6 shrink-0 text-right font-mono text-xs tabular-nums'>
<span className='text-muted-foreground/80 w-6 shrink-0 text-right text-xs tabular-nums'>
{row.rank}.
</span>
<span className='shrink-0'>
@@ -99,7 +99,7 @@ function ModelList(props: {
<p
className={
compact
? 'text-muted-foreground/80 truncate text-[11px] italic'
? 'text-muted-foreground/80 truncate text-xs italic'
: 'text-muted-foreground/80 truncate text-xs italic'
}
>
@@ -113,8 +113,8 @@ function ModelList(props: {
<div
className={
compact
? 'text-foreground font-mono text-xs font-semibold tabular-nums'
: 'text-foreground font-mono text-sm font-semibold tabular-nums'
? 'text-foreground text-xs font-semibold tabular-nums'
: 'text-foreground text-sm font-semibold tabular-nums'
}
>
{formatTokens(row.total_tokens)}
@@ -129,7 +129,7 @@ function ModelList(props: {
</div>
<GrowthText
value={row.growth_pct}
className={compact ? 'text-[10px]' : 'text-[11px]'}
className={compact ? 'text-xs' : 'text-xs'}
/>
</div>
</li>
@@ -177,10 +177,10 @@ export function ModelsSection(props: ModelsSectionProps) {
</p>
</div>
<div className='shrink-0 text-right'>
<div className='text-foreground font-mono text-2xl font-semibold tabular-nums'>
<div className='text-foreground text-2xl font-semibold tabular-nums'>
{formatTokens(totalTokens)}
</div>
<div className='text-muted-foreground/80 text-[10px] font-medium tracking-widest uppercase'>
<div className='text-muted-foreground/80 text-xs font-medium tracking-widest uppercase'>
{t('tokens')}
</div>
</div>
@@ -210,7 +210,7 @@ export function ModelsSection(props: ModelsSectionProps) {
<div className='border-t'>
<header className='px-5 pt-4 pb-2'>
<h3 className='text-foreground inline-flex items-center gap-2 text-sm font-semibold'>
<Trophy className='size-3.5 text-amber-500' />
<Trophy className='text-warning size-3.5' />
{t('LLM Leaderboard')}
</h3>
<p className='text-muted-foreground/80 mt-0.5 text-xs'>
@@ -46,7 +46,7 @@ export function PulseSection(props: PulseSectionProps) {
<PulseCard
title={t('Trending up')}
description={t('Models climbing the leaderboard')}
icon={<TrendingUp className='size-4 text-emerald-500' />}
icon={<TrendingUp className='text-success size-4' />}
>
{props.movers.length === 0 ? (
<PulseEmpty label={t('No notable climbers right now')} />
@@ -62,7 +62,7 @@ export function PulseSection(props: PulseSectionProps) {
<PulseCard
title={t('Trending down')}
description={t('Models losing positions')}
icon={<TrendingDown className='size-4 text-rose-500' />}
icon={<TrendingDown className='text-destructive size-4' />}
>
{props.droppers.length === 0 ? (
<PulseEmpty label={t('No notable drops right now')} />
@@ -119,7 +119,7 @@ function MoverRow(props: { row: RankingMover; intent: 'up' | 'down' }) {
>
{props.row.model_name}
</ModelLink>
<p className='text-muted-foreground/80 truncate text-[11px]'>
<p className='text-muted-foreground/80 truncate text-xs'>
#{props.row.current_rank} ·{' '}
<VendorLink vendor={props.row.vendor}>
{props.row.vendor.toLowerCase()}
@@ -128,10 +128,8 @@ function MoverRow(props: { row: RankingMover; intent: 'up' | 'down' }) {
</div>
<span
className={cn(
'inline-flex shrink-0 items-center gap-0.5 font-mono text-xs font-semibold tabular-nums',
props.intent === 'up'
? 'text-emerald-600 dark:text-emerald-400'
: 'text-rose-600 dark:text-rose-400'
'inline-flex shrink-0 items-center gap-0.5 text-xs font-semibold tabular-nums',
props.intent === 'up' ? 'text-success' : 'text-destructive'
)}
>
{props.intent === 'up' ? (
@@ -22,7 +22,10 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { DataTableRowActionMenu, StaticDataTable } from '@/components/data-table'
import {
DataTableRowActionMenu,
StaticDataTable,
} from '@/components/data-table'
import {
sideDrawerContentClassName,
sideDrawerFormClassName,
@@ -68,7 +68,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
id: 'price',
header: t('Price'),
cell: ({ row }) => (
<span className='font-semibold text-emerald-600'>
<span className='text-success font-semibold'>
${Number(row.original.plan.price_amount || 0).toFixed(2)}
</span>
),
@@ -74,14 +74,13 @@ const INSTANCE_SKELETON_KEYS = [
]
const STATUS_CLASS_NAME: Record<SystemInstanceStatus, string> = {
online:
'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300',
stale: 'bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300',
online: 'bg-success/10 text-success',
stale: 'bg-warning/10 text-warning',
}
const STATUS_DOT_CLASS_NAME: Record<SystemInstanceStatus, string> = {
online: 'bg-emerald-500',
stale: 'bg-amber-500',
online: 'bg-success',
stale: 'bg-warning',
}
function roleLabel(instance: SystemInstance) {
@@ -136,9 +135,9 @@ function formatBytes(bytes?: number): string {
function ringColorClass(percent: number | null) {
if (percent === null) return 'text-muted-foreground/40'
if (percent >= 90) return 'text-red-500'
if (percent >= 70) return 'text-amber-500'
return 'text-emerald-500'
if (percent >= 90) return 'text-destructive'
if (percent >= 70) return 'text-warning'
return 'text-success'
}
type RingProgressProps = {
@@ -205,9 +204,7 @@ function ResourceCell(props: ResourceCellProps) {
const content = (
<div className='flex items-center gap-2'>
<RingProgress percent={percent} />
<span className='font-mono text-[11px] tabular-nums'>
{formatPercent(props.value)}
</span>
<span className='text-xs tabular-nums'>{formatPercent(props.value)}</span>
</div>
)
@@ -304,7 +301,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
>
<Badge
variant='outline'
className='border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/15 dark:text-amber-300'
className='border-warning/30 bg-warning/10 text-warning'
>
<AlertTriangle
className='size-3'
@@ -328,7 +325,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
<div className='mb-1 font-medium'>
{t('Example')}
</div>
<code className='bg-muted block rounded-md px-2 py-1.5 font-mono text-[11px] break-all'>
<code className='bg-muted block rounded-md px-2 py-1.5 font-mono text-xs break-all'>
NODE_NAME=new-api-master-1
</code>
</div>
@@ -342,7 +339,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
</Popover>
)}
</div>
<div className='text-muted-foreground truncate font-mono text-[11px]'>
<div className='text-muted-foreground truncate font-mono text-xs'>
{instance.info?.host?.hostname || '-'}
</div>
</div>
@@ -53,26 +53,23 @@ const STATUS_VARIANT: Record<SystemTaskStatus, 'secondary' | 'destructive'> = {
}
const STATUS_CLASS_NAME: Record<SystemTaskStatus, string> = {
pending:
'bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300',
running:
'bg-sky-50 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300 [&_span]:bg-sky-500',
succeeded:
'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300',
pending: 'bg-warning/10 text-warning',
running: 'bg-info/10 text-info [&_span]:bg-info',
succeeded: 'bg-success/10 text-success',
failed: '',
}
const STATUS_DOT_CLASS_NAME: Record<SystemTaskStatus, string> = {
pending: 'bg-amber-500',
running: 'bg-sky-500',
succeeded: 'bg-emerald-500',
pending: 'bg-warning',
running: 'bg-info',
succeeded: 'bg-success',
failed: 'bg-destructive',
}
const PROGRESS_BAR_CLASS_NAME: Record<SystemTaskStatus, string> = {
pending: '[&_[data-slot=progress-indicator]]:bg-amber-500',
running: '[&_[data-slot=progress-indicator]]:bg-sky-500',
succeeded: '[&_[data-slot=progress-indicator]]:bg-emerald-500',
pending: '[&_[data-slot=progress-indicator]]:bg-warning',
running: '[&_[data-slot=progress-indicator]]:bg-info',
succeeded: '[&_[data-slot=progress-indicator]]:bg-success',
failed: '[&_[data-slot=progress-indicator]]:bg-destructive',
}
@@ -142,7 +139,7 @@ function SystemTasksTable(props: SystemTasksTableProps) {
<div className='font-medium'>
{t(TYPE_LABEL[task.type] ?? task.type)}
</div>
<div className='text-muted-foreground font-mono text-[11px]'>
<div className='text-muted-foreground font-mono text-xs'>
{TYPE_DISPLAY_ID[task.type] ?? task.type}
</div>
</div>
@@ -256,7 +253,7 @@ export function SystemTasksPanel() {
<span
className={cn(
'size-1.5 rounded-full',
hasActiveTasks ? 'bg-emerald-500' : 'bg-muted-foreground/40'
hasActiveTasks ? 'bg-success' : 'bg-muted-foreground/40'
)}
aria-hidden='true'
/>
@@ -42,8 +42,8 @@ export function FormDirtyIndicator({
return (
<SettingsPageTitleStatusPortal>
<span className='inline-flex h-5 items-center gap-1.5 rounded-full bg-amber-500/10 px-2 text-[11px] font-medium whitespace-nowrap text-amber-700 ring-1 ring-amber-500/20 ring-inset dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/20'>
<span className='size-1.5 rounded-full bg-amber-500 dark:bg-amber-300' />
<span className='bg-warning/10 text-warning ring-warning/25 inline-flex h-5 items-center gap-1.5 rounded-full px-2 text-xs font-medium whitespace-nowrap ring-1 ring-inset'>
<span className='bg-warning size-1.5 rounded-full' />
{message ? t(message) : t('Unsaved changes')}
</span>
</SettingsPageTitleStatusPortal>
@@ -532,7 +532,7 @@ export function WaffoPancakeSettingsSection({
<>
<div className='relative flex items-center py-1'>
<div className='flex-1 border-t' />
<span className='text-muted-foreground px-3 text-[10px] font-medium tracking-[0.2em] uppercase'>
<span className='text-muted-foreground px-3 text-xs font-medium tracking-[0.2em] uppercase'>
{t('or pick existing')}
</span>
<div className='flex-1 border-t' />
@@ -404,7 +404,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
<div className={sideDrawerFormClassName('gap-5')}>
<section className='space-y-2'>
<h3 className='text-sm font-semibold'>{t('The two roles of a group')}</h3>
<h3 className='text-sm font-semibold'>
{t('The two roles of a group')}
</h3>
<div className='text-muted-foreground space-y-2 text-sm leading-6'>
<p>
{t(
@@ -416,7 +418,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
{t('Token group')}
</span>
{': '}
{t('decides which channels are used and which base ratio applies.')}
{t(
'decides which channels are used and which base ratio applies.'
)}
</p>
<p>
<span className='text-foreground font-medium'>
@@ -431,7 +435,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
</section>
<section className='space-y-2'>
<h3 className='text-sm font-semibold'>{t('How a call is priced')}</h3>
<h3 className='text-sm font-semibold'>
{t('How a call is priced')}
</h3>
<ol className='text-muted-foreground list-decimal space-y-2 pl-5 text-sm leading-6'>
<li>
<span className='text-foreground font-medium'>
@@ -453,7 +459,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
<span className='text-foreground font-medium'>
{t('Charge.')}
</span>{' '}
{t('Cost = model price × that one ratio. Nothing else from the group settings enters the formula.')}
{t(
'Cost = model price × that one ratio. Nothing else from the group settings enters the formula.'
)}
</li>
</ol>
<p className='text-muted-foreground text-sm leading-6'>
@@ -466,7 +474,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
<section className='space-y-3'>
<h3 className='text-sm font-semibold'>{t('Worked example')}</h3>
<p className='text-muted-foreground text-sm leading-6'>
{t('The admin configured three groups and one special ratio rule:')}
{t(
'The admin configured three groups and one special ratio rule:'
)}
</p>
<div className='overflow-hidden rounded-lg border'>
@@ -529,7 +539,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
</div>
<div className='space-y-2 p-3'>
<GuideStepRow chip='1'>
{t('Billing group = premium (the token has a group, so use it)')}
{t(
'Billing group = premium (the token has a group, so use it)'
)}
</GuideStepRow>
<GuideStepRow chip='2'>
{t(
@@ -550,7 +562,9 @@ function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
</div>
<div className='space-y-2 p-3'>
<GuideStepRow chip='1'>
{t('Billing group = default (the token has a group, so use it)')}
{t(
'Billing group = default (the token has a group, so use it)'
)}
</GuideStepRow>
<GuideStepRow chip='2'>
{t(
@@ -29,6 +29,7 @@ import { useTranslation } from 'react-i18next'
import { StaticDataTable } from '@/components/data-table/static/static-data-table'
import { StaticRowActions } from '@/components/data-table/static/static-row-actions'
import { Dialog } from '@/components/dialog'
import {
sideDrawerContentClassName,
sideDrawerFormClassName,
@@ -49,7 +50,6 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import { Dialog } from '@/components/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
@@ -1100,10 +1100,13 @@ function GroupOverrideDialog({
<p className='text-muted-foreground text-xs'>
{baseRatio !== undefined
? t('(instead of {{ratio}})', { ratio: baseRatio })
: t('Multiplier applied when {{userGroup}} uses {{targetGroup}}', {
userGroup: userGroup || t('this user group'),
targetGroup: targetGroup || t('this token group'),
})}
: t(
'Multiplier applied when {{userGroup}} uses {{targetGroup}}',
{
userGroup: userGroup || t('this user group'),
targetGroup: targetGroup || t('this token group'),
}
)}
</p>
</div>
</div>
@@ -83,7 +83,7 @@ export function useUpstreamRatioSyncColumns(
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<AlertTriangle className='h-3.5 w-3.5 shrink-0 text-amber-500' />
<AlertTriangle className='text-warning h-3.5 w-3.5 shrink-0' />
</TooltipTrigger>
<TooltipContent>
<p>
@@ -199,7 +199,7 @@ export function useUpstreamRatioSyncColumns(
{displayName}
</span>
{selectableCount > 0 && (
<span className='bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-[11px] leading-none font-normal tabular-nums'>
<span className='bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-xs leading-none font-normal tabular-nums'>
{selectedCount}/{selectableCount}
</span>
)}
@@ -361,7 +361,7 @@ function renderUpstreamValue(args: RenderUpstreamValueArgs) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<AlertTriangle className='h-3.5 w-3.5 shrink-0 text-amber-500' />
<AlertTriangle className='text-warning h-3.5 w-3.5 shrink-0' />
</TooltipTrigger>
<TooltipContent>
<p>{t('This data may be unreliable, use with caution')}</p>
@@ -22,7 +22,7 @@ import { Zap } from 'lucide-react'
import { useState } from 'react'
import { DataTableColumnHeader } from '@/components/data-table'
import { StatusBadge } from '@/components/status-badge'
import { StatusBadge, tintedBadgeClassMap } from '@/components/status-badge'
import {
Tooltip,
TooltipContent,
@@ -90,7 +90,7 @@ export function createTimestampColumn<T>(config: {
return <span className='text-muted-foreground/60 text-xs'>-</span>
}
return (
<span className='font-mono text-xs tabular-nums'>
<span className='text-xs tabular-nums'>
{formatTimestampToDate(timestamp, unit)}
</span>
)
@@ -137,22 +137,16 @@ export function createDurationColumn<T>(config: {
const variant =
duration.durationSec > warningThresholdSec ? 'danger' : 'success'
const durationBgMap: Record<string, string> = {
success:
'border border-emerald-200/40 bg-emerald-50/35 !text-emerald-600 dark:border-emerald-900/40 dark:bg-emerald-950/15 dark:!text-emerald-400',
warning:
'border border-amber-200/45 bg-amber-50/35 !text-amber-600 dark:border-amber-900/40 dark:bg-amber-950/15 dark:!text-amber-400',
danger:
'border border-rose-200/50 bg-rose-50/35 !text-red-600 dark:border-rose-900/40 dark:bg-rose-950/15 dark:!text-red-400',
}
return (
<StatusBadge
label={`${duration.durationSec.toFixed(1)}s`}
variant={variant}
size='sm'
copyable={false}
className={cn('rounded-md font-mono', durationBgMap[variant])}
className={cn(
'rounded-md tabular-nums',
tintedBadgeClassMap[variant]
)}
/>
)
},
@@ -225,7 +219,7 @@ export function createFailReasonColumn<T>(config: {
onClick={() => setDialogOpen(true)}
title={cellTitle}
>
<span className='truncate leading-snug text-red-600 group-hover:underline dark:text-red-400'>
<span className='text-destructive truncate leading-snug group-hover:underline'>
{failReason}
</span>
</button>
@@ -21,7 +21,11 @@ import { CircleAlert, GitBranch, Sparkles, KeyRound } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
import {
StatusBadge,
tintedBadgeClassMap,
type StatusBadgeProps,
} from '@/components/status-badge'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import {
Popover,
@@ -304,7 +308,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
return (
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='truncate font-mono text-xs tabular-nums'>
<span className='truncate text-xs tabular-nums'>
{formatTimestampToDate(timestamp)}
</span>
<StatusBadge
@@ -312,7 +316,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
variant={config.color as StatusBadgeProps['variant']}
size='sm'
copyable={false}
className='!text-xs [&_span]:!text-xs'
className='text-xs [&_span]:text-xs'
/>
</div>
)
@@ -402,7 +406,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
}
>
<GitBranch
className='size-3.5 text-amber-500'
className='text-warning size-3.5'
aria-hidden='true'
/>
</PopoverTrigger>
@@ -423,7 +427,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
{affinity && (
<button
type='button'
className='absolute -top-1 -right-1 leading-none text-amber-500'
className='text-warning absolute -top-1 -right-1 leading-none'
onClick={(e) => {
e.stopPropagation()
setAffinityTarget({
@@ -443,7 +447,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
)}
</div>
{log.channel_name && (
<span className='text-muted-foreground/70 truncate [font-family:var(--font-body)] !text-xs'>
<span className='text-muted-foreground/70 truncate text-xs'>
{channelName}
</span>
)}
@@ -510,7 +514,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
<AvatarFallback
className={cn(
'text-[11px] font-semibold',
'text-xs font-semibold',
!sensitiveVisible && 'bg-muted text-muted-foreground'
)}
style={
@@ -577,7 +581,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
copyText={sensitiveVisible ? tokenName : undefined}
size='sm'
showDot={false}
className='border-border/60 bg-muted/30 text-foreground h-6 max-w-full gap-1.5 overflow-hidden rounded-md border px-2 py-0.5 [font-family:var(--font-body)]'
className='border-border/60 bg-muted/30 text-foreground h-6 max-w-full gap-1.5 overflow-hidden rounded-md border px-2 py-0.5'
/>
</TooltipTrigger>
{sensitiveVisible && tokenName.length > 16 && (
@@ -588,7 +592,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
</Tooltip>
</TooltipProvider>
{metaParts.length > 0 && (
<span className='text-muted-foreground/60 truncate [font-family:var(--font-body)] !text-xs'>
<span className='text-muted-foreground/60 truncate text-xs'>
{metaParts.join(' · ')}
</span>
)}
@@ -637,16 +641,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
? getFirstResponseTimeColor(frt / 1000)
: 'neutral'
const timingBgMap: Record<string, string> = {
success:
'border border-emerald-200/40 bg-emerald-50/35 !text-emerald-600 dark:border-emerald-900/40 dark:bg-emerald-950/15 dark:!text-emerald-400',
warning:
'border border-amber-200/45 bg-amber-50/35 !text-amber-600 dark:border-amber-900/40 dark:bg-amber-950/15 dark:!text-amber-400',
danger:
'border border-rose-200/50 bg-rose-50/35 !text-red-600 dark:border-rose-900/40 dark:bg-rose-950/15 dark:!text-red-400',
neutral:
'border border-border/60 bg-muted/30 dark:border-border/40 dark:bg-muted/20',
}
const timingClass = (variant: string) =>
tintedBadgeClassMap[
(variant in tintedBadgeClassMap
? variant
: 'neutral') as keyof typeof tintedBadgeClassMap
]
return (
<div className='flex flex-col gap-1'>
@@ -656,7 +656,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
variant={timeVariant as StatusBadgeProps['variant']}
size='sm'
copyable={false}
className={cn('rounded-md font-mono', timingBgMap[timeVariant])}
className={cn(
'rounded-md tabular-nums',
timingClass(timeVariant)
)}
/>
{log.is_stream &&
(frt != null && frt > 0 ? (
@@ -667,8 +670,8 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
showDot={false}
copyable={false}
className={cn(
'rounded-md font-mono',
timingBgMap[frtVariant]
'rounded-md tabular-nums',
timingClass(frtVariant)
)}
/>
) : (
@@ -678,12 +681,15 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
size='sm'
showDot={false}
copyable={false}
className={cn('rounded-md font-mono', timingBgMap.neutral)}
className={cn(
'rounded-md tabular-nums',
tintedBadgeClassMap.neutral
)}
/>
))}
</div>
<div className='flex items-center gap-1 [font-family:var(--font-body)] !text-xs leading-none'>
<span className='text-muted-foreground/60 [font-family:var(--font-body)] !text-xs leading-none'>
<div className='flex items-center gap-1 text-xs leading-none'>
<span className='text-muted-foreground/60 text-xs leading-none'>
{log.is_stream ? t('Stream') : t('Non-stream')}
{tokensPerSecond != null && (
<>
@@ -701,7 +707,9 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={<CircleAlert className='size-3 text-red-500' />}
render={
<CircleAlert className='text-destructive size-3' />
}
></TooltipTrigger>
<TooltipContent>
<div className='space-y-0.5 text-xs'>
@@ -751,12 +759,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
return (
<div className='flex flex-col gap-0.5'>
<span className='font-mono text-xs font-medium tabular-nums'>
<span className='text-xs font-medium tabular-nums'>
{promptTokens.toLocaleString()} /{' '}
{completionTokens.toLocaleString()}
</span>
{(cacheReadTokens > 0 || cacheWriteTokens > 0) && (
<div className='flex items-center gap-1 text-[11px]'>
<div className='flex items-center gap-1 text-xs'>
{cacheReadTokens > 0 && (
<span className='text-muted-foreground/60'>
{t('Cache')} {cacheReadTokens.toLocaleString()}
@@ -815,7 +823,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
return (
<div className='flex flex-col gap-0.5'>
<span className='border-border/80 bg-muted/60 inline-flex h-6 w-fit items-center rounded-md border px-2 [font-family:var(--font-body)] text-sm leading-none font-semibold tabular-nums'>
<span className='border-border/80 bg-muted/60 inline-flex h-6 w-fit items-center rounded-md border px-2 text-sm leading-none font-semibold tabular-nums'>
{quotaDisplay.prefix && (
<span className='mr-1'>{quotaDisplay.prefix}</span>
)}
@@ -853,7 +861,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
primary.muted
? 'text-muted-foreground/60'
: primary.danger
? 'text-red-600 dark:text-red-400'
? 'text-destructive'
: 'text-foreground'
)}
>
@@ -95,7 +95,7 @@ export function useDrawingLogsColumns(
return (
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='truncate font-mono text-xs tabular-nums'>
<span className='truncate text-xs tabular-nums'>
{formatTimestampToDate(submitTime, 'milliseconds')}
</span>
<StatusBadge
@@ -151,7 +151,7 @@ export function useDrawingLogsColumns(
copyText={mjId}
variant='neutral'
size='sm'
className='border-border/60 bg-muted/30 !text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
className='border-border/60 bg-muted/30 text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
/>
</div>
)
@@ -102,15 +102,15 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
return (
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='truncate font-mono text-xs tabular-nums'>
<span className='truncate text-xs tabular-nums'>
{formatTimestampToDate(submitTime, 'seconds')}
</span>
{log.finish_time ? (
<span className='text-muted-foreground/60 truncate font-mono text-[11px] tabular-nums'>
<span className='text-muted-foreground/60 truncate text-xs tabular-nums'>
{formatTimestampToDate(log.finish_time, 'seconds')}
</span>
) : (
<span className='text-muted-foreground/50 text-[11px]'>-</span>
<span className='text-muted-foreground/50 text-xs'>-</span>
)}
</div>
)
@@ -143,7 +143,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
<AvatarFallback
className={cn(
'text-[11px] font-semibold',
'text-xs font-semibold',
!sensitiveVisible && 'bg-muted text-muted-foreground'
)}
style={
@@ -179,9 +179,9 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
copyText={taskId}
variant='neutral'
size='sm'
className='border-border/60 bg-muted/30 !text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
className='border-border/60 bg-muted/30 text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
/>
<span className='text-muted-foreground/60 truncate text-[11px]'>
<span className='text-muted-foreground/60 truncate text-xs'>
{t(log.platform)} · {t(taskActionMapper.getLabel(log.action))}
</span>
</div>
@@ -41,7 +41,7 @@ function StatBadge(props: {
<span className='border-border/60 bg-muted/25 inline-flex h-7 items-center gap-2 rounded-md border px-2.5 text-xs shadow-xs'>
<span className={cn('h-3.5 w-0.5 rounded-full', props.accent)} />
<span className='text-muted-foreground'>{props.label}</span>
<span className='text-foreground/85 font-mono font-semibold tabular-nums'>
<span className='text-foreground/85 font-semibold tabular-nums'>
{props.value}
</span>
</span>
@@ -91,17 +91,17 @@ export function CommonLogsStats() {
<StatBadge
label={t('Usage')}
value={sensitiveVisible ? formatLogQuota(stats?.quota || 0) : '••••'}
accent='bg-sky-500/70'
accent='bg-chart-1/70'
/>
<StatBadge
label={t('RPM')}
value={stats?.rpm || 0}
accent='bg-rose-500/65'
accent='bg-chart-5/70'
/>
<StatBadge
label={t('TPM')}
value={stats?.tpm || 0}
accent='bg-slate-400/70'
accent='bg-muted-foreground/50'
/>
</div>
)
@@ -77,9 +77,9 @@ const CHANNEL_FIELD_LABELS: Record<string, string> = {
function timingTextColorClass(
variant: 'success' | 'warning' | 'danger'
): string {
if (variant === 'success') return 'text-emerald-600'
if (variant === 'warning') return 'text-amber-600'
return 'text-rose-600'
if (variant === 'success') return 'text-success'
if (variant === 'warning') return 'text-warning'
return 'text-destructive'
}
function DetailRow(props: {
@@ -628,7 +628,10 @@ export function DetailsDialog(props: DetailsDialogProps) {
label={t('IP Address')}
value={
<span className='flex items-center gap-1'>
<Globe className='size-3 text-amber-500' aria-hidden='true' />
<Globe
className='text-muted-foreground size-3'
aria-hidden='true'
/>
{props.log.ip}
</span>
}
@@ -814,7 +817,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
/>
))}
{showLegacyTopupWarning && (
<div className='flex items-start gap-1.5 text-xs text-amber-600 dark:text-amber-400'>
<div className='text-warning flex items-start gap-1.5 text-xs'>
<Info className='mt-0.5 size-3.5 shrink-0' aria-hidden='true' />
<span>
{t(
@@ -1030,9 +1033,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
value={
<span className='flex items-center gap-1'>
{other.admin_info.local_count_tokens ? (
<Monitor className='size-3 text-blue-500' />
<Monitor className='text-muted-foreground size-3' />
) : (
<Cloud className='size-3 text-emerald-500' />
<Cloud className='text-muted-foreground size-3' />
)}
<span className='text-xs'>
{other.admin_info.local_count_tokens
@@ -1080,7 +1083,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
)}
{Array.isArray(other.stream_status.errors) &&
other.stream_status.errors.length > 0 && (
<pre className='bg-background/60 mt-1 max-h-32 overflow-y-auto rounded border p-2 font-mono text-[11px] leading-relaxed wrap-break-word whitespace-pre-wrap'>
<pre className='bg-background/60 mt-1 max-h-32 overflow-y-auto rounded border p-2 font-mono text-xs leading-relaxed wrap-break-word whitespace-pre-wrap'>
{other.stream_status.errors.join('\n')}
</pre>
)}
@@ -1155,7 +1158,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
className='shrink-0 font-medium'
copyable={false}
/>
<span className='min-w-0 font-mono text-[11px] leading-relaxed break-all sm:wrap-break-word'>
<span className='min-w-0 font-mono text-xs leading-relaxed break-all sm:wrap-break-word'>
{parsed.content}
</span>
</div>
@@ -120,7 +120,7 @@ export function LogsFilterToolbar<TData>(props: LogsFilterToolbarProps<TData>) {
>
{advancedOpen ? t('Collapse') : t('Expand')}
{activeAdvancedCount > 0 && (
<Badge className='ml-0.5 size-5 justify-center p-0 text-[10px]'>
<Badge className='ml-0.5 size-5 justify-center p-0 text-xs'>
{activeAdvancedCount}
</Badge>
)}
@@ -157,7 +157,7 @@ export function LogsFilterToolbar<TData>(props: LogsFilterToolbarProps<TData>) {
>
{t('Filter')}
{activeMobileFilterCount > 0 && (
<Badge className='ml-0.5 size-5 justify-center p-0 text-[10px]'>
<Badge className='ml-0.5 size-5 justify-center p-0 text-xs'>
{activeMobileFilterCount}
</Badge>
)}
@@ -132,7 +132,7 @@ function ModelBadgeContent(props: ModelBadgeProps) {
showDot={!provider}
autoColor={provider ? undefined : props.modelName}
className={cn(
'border-border/60 bg-muted/30 h-6 max-w-none gap-1.5 rounded-md border px-2 [font-family:var(--font-body)]',
'border-border/60 bg-muted/30 h-6 max-w-none gap-1.5 rounded-md border px-2',
provider && 'text-foreground',
props.className
)}
@@ -41,10 +41,8 @@ import { getLogTypeConfig } from '../lib/utils'
import type { LogCategory } from '../types'
const logTypeRowTint: Record<number, string> = {
[LOG_TYPE_ENUM.ERROR]:
'bg-rose-50/40 dark:bg-rose-950/20 border-rose-200/50 dark:border-rose-900/30',
[LOG_TYPE_ENUM.REFUND]:
'bg-blue-50/30 dark:bg-blue-950/15 border-blue-200/50 dark:border-blue-900/30',
[LOG_TYPE_ENUM.ERROR]: 'bg-destructive/5 border-destructive/20',
[LOG_TYPE_ENUM.REFUND]: 'bg-info/5 border-info/20',
}
interface UsageLogsMobileListProps<TData> {
@@ -129,7 +127,7 @@ function SummaryField<TData>({
<div
className={cn('bg-muted/20 min-w-0 rounded-md px-2 py-1.5', className)}
>
<div className='text-muted-foreground mb-1 text-[11px] leading-none font-medium select-none'>
<div className='text-muted-foreground mb-1 text-xs leading-none font-medium select-none'>
{label}
</div>
<CompactCell
@@ -156,7 +154,7 @@ function MobileLogTimeStatus({
return (
<div className='space-y-1'>
<div className='font-mono text-xs leading-tight tabular-nums'>
<div className='text-xs leading-tight tabular-nums'>
{formatTimestampToDate(timestamp)}
</div>
<div
@@ -200,7 +198,7 @@ function CommonLogsCard<TData>({
<div className='grid grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)] gap-1.5'>
<div className='bg-muted/20 min-w-0 rounded-md px-2 py-1.5'>
<div className='text-muted-foreground mb-1 text-[11px] leading-none font-medium select-none'>
<div className='text-muted-foreground mb-1 text-xs leading-none font-medium select-none'>
{t('Time')}
</div>
<MobileLogTimeStatus
@@ -217,7 +215,7 @@ function CommonLogsCard<TData>({
<SummaryField
label={t('Token')}
cell={cells.get('token_name')}
valueClassName='[&_.flex-col]:max-w-none [&_.flex-col>*:not(:first-child)]:text-[11px] [&_.flex-col>*:not(:first-child)]:leading-none'
valueClassName='[&_.flex-col]:max-w-none [&_.flex-col>*:not(:first-child)]:text-xs [&_.flex-col>*:not(:first-child)]:leading-none'
/>
<SummaryField
label={t('Timing')}
@@ -48,13 +48,13 @@ import { UsageLogsMobileList } from './usage-logs-mobile-card'
const route = getRouteApi('/_authenticated/usage-logs/$section')
const logTypeRowTint: Record<number, string> = {
[LOG_TYPE_ENUM.ERROR]: 'bg-rose-50/40 dark:bg-rose-950/20',
[LOG_TYPE_ENUM.REFUND]: 'bg-blue-50/30 dark:bg-blue-950/15',
[LOG_TYPE_ENUM.ERROR]: 'bg-destructive/5',
[LOG_TYPE_ENUM.REFUND]: 'bg-info/5',
}
// Warning tint for logs where a quota conversion saturated (admin-only marker).
// Takes precedence over the per-type tint since it flags a billing anomaly.
const quotaSaturationRowTint = 'bg-amber-50/60 dark:bg-amber-950/25'
const quotaSaturationRowTint = 'bg-warning/10'
function getColumnVisibilityStorageKey(
logCategory: LogCategory,
@@ -44,9 +44,10 @@ import type { User } from '../types'
import { DataTableRowActions } from './data-table-row-actions'
function getQuotaProgressColor(percentage: number): string {
if (percentage <= 10) return '[&_[data-slot=progress-indicator]]:bg-rose-500'
if (percentage <= 30) return '[&_[data-slot=progress-indicator]]:bg-amber-500'
return '[&_[data-slot=progress-indicator]]:bg-emerald-500'
if (percentage <= 10)
return '[&_[data-slot=progress-indicator]]:bg-destructive'
if (percentage <= 30) return '[&_[data-slot=progress-indicator]]:bg-warning'
return '[&_[data-slot=progress-indicator]]:bg-success'
}
export function useUsersColumns(): ColumnDef<User>[] {
@@ -87,7 +87,7 @@ export function AffiliateRewardsCard({
[t('Invites'), String(user?.aff_count ?? 0)],
].map(([label, value]) => (
<div key={label}>
<div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
{label}
</div>
<div className='mt-0.5 truncate text-sm font-semibold tabular-nums'>
@@ -50,9 +50,7 @@ export function TransferDialog({
transferring,
}: TransferDialogProps) {
const { t } = useTranslation()
const currencyConfig = useSystemConfigStore(
(state) => state.config.currency
)
const currencyConfig = useSystemConfigStore((state) => state.config.currency)
const minimumQuota = Math.ceil(
currencyConfig.quotaPerUnit > 0
? currencyConfig.quotaPerUnit
@@ -353,7 +353,7 @@ export function RechargeFormCard({
{method.name}
</span>
{disabledLabel && (
<span className='text-muted-foreground max-w-full truncate text-[11px] leading-4 font-normal'>
<span className='text-muted-foreground max-w-full truncate text-xs leading-4 font-normal'>
{disabledLabel}
</span>
)}
@@ -435,7 +435,7 @@ export function RechargeFormCard({
{method.name}
</span>
{disabledLabel && (
<span className='text-muted-foreground max-w-full truncate text-[11px] leading-4 font-normal'>
<span className='text-muted-foreground max-w-full truncate text-xs leading-4 font-normal'>
{disabledLabel}
</span>
)}
@@ -80,7 +80,7 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
</div>
</div>
<div className='text-foreground mt-1.5 font-mono text-base font-bold tracking-tight break-all tabular-nums sm:mt-2 sm:text-2xl'>
<div className='text-foreground mt-1.5 text-base font-bold tracking-tight break-all tabular-nums sm:mt-2 sm:text-2xl'>
{item.value}
</div>
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
+16 -3
View File
@@ -20,14 +20,14 @@ import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
import { convertDetectedLanguage } from './languages'
import { convertDetectedLanguage, toIntlLocale } from './languages'
import en from './locales/en.json'
import fr from './locales/fr.json'
import ja from './locales/ja.json'
import ru from './locales/ru.json'
import vi from './locales/vi.json'
import zhCN from './locales/zh.json'
import zhTW from './locales/zh-TW.json'
import zhCN from './locales/zh.json'
export const resources = {
en,
@@ -36,7 +36,7 @@ export const resources = {
ru,
ja,
vi,
zhTW
zhTW,
} as const
i18n
@@ -61,4 +61,17 @@ i18n
},
})
// Keep <html lang> in sync with the active UI language. Han unification means
// zh-CN / zh-TW / ja share code points but need different glyphs; without a
// correct lang attribute browsers guess (index.html ships lang="en") and CJK
// text renders with the wrong regional font.
function syncDocumentLanguage(language?: string) {
if (typeof document === 'undefined') return
const locale = toIntlLocale(language ?? i18n.resolvedLanguage)
if (locale) document.documentElement.lang = locale
}
i18n.on('languageChanged', syncDocumentLanguage)
syncDocumentLanguage(i18n.resolvedLanguage)
export default i18n
+8 -3
View File
@@ -24,7 +24,7 @@ export const INTERFACE_LANGUAGE_OPTIONS = [
{ code: 'ru', label: 'Русский' },
{ code: 'ja', label: '日本語' },
{ code: 'vi', label: 'Tiếng Việt' },
{ code: 'zhTW', label: '繁體中文' }
{ code: 'zhTW', label: '繁體中文' },
] as const
export type InterfaceLanguageCode =
@@ -34,10 +34,15 @@ export function normalizeInterfaceLanguage(value?: string | null): string {
if (!value) return 'en'
let normalized = value.trim().replaceAll('_', '-').toLowerCase()
if (value === 'zh-TW' || value === 'zh-HK' || value === 'zh-MO' || value === 'zhTW') {
if (
value === 'zh-TW' ||
value === 'zh-HK' ||
value === 'zh-MO' ||
value === 'zhTW'
) {
normalized = 'zhTW'
}
if (value === 'zh-CN' || value === 'zh-Hans' || value === "zhCN") {
if (value === 'zh-CN' || value === 'zh-Hans' || value === 'zhCN') {
normalized = 'zhCN'
}
+5 -9
View File
@@ -163,17 +163,13 @@ const TAG_COLORS = [
] as const
/**
* Convert string to a stable semantic color
* Used for model tags, group badges, user avatars, etc.
* Same string always returns the same color
* Convert string to a stable semantic color.
* Only for avatar identity tinting (via `getAvatarColorClass`), where a subtle
* per-user hue aids recognition. Do NOT use it to color badges/tags text
* badges speak only the five semantic voices (see `components/status-badge`).
*
* @param str - Input string (model name, group name, username, etc.)
* @param str - Input string (username, etc.)
* @returns Semantic color name from TAG_COLORS
*
* @example
* stringToColor('gpt-4') // 'blue'
* stringToColor('claude-3') // 'purple'
* stringToColor('default') // 'green'
*/
export function stringToColor(str: string): SemanticColor {
let sum = 0
+6 -72
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 type { Transition, Variants } from 'motion/react'
import type { Transition } from 'motion/react'
const EASE_OUT_CUBIC = [0.33, 1, 0.68, 1] as const
@@ -36,87 +36,21 @@ export const MOTION_TRANSITION: Record<string, Transition> = {
}
export const MOTION_VARIANTS = {
// Admin surfaces keep page transitions to a bare opacity fade: no vertical
// shift, no blur. Anything more reads as lag on high-frequency workflows.
pageEnter: {
initial: { opacity: 0, y: 8, filter: 'blur(4px)' },
animate: { opacity: 1, y: 0, filter: 'blur(0px)' },
exit: { opacity: 0, y: -4, filter: 'blur(2px)' },
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
},
fadeIn: {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
},
scaleIn: {
initial: { opacity: 0, scale: 0.96 },
animate: { opacity: 1, scale: 1 },
exit: { opacity: 0, scale: 0.96 },
},
slideUp: {
initial: { opacity: 0, y: 16 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 16 },
},
slideDown: {
initial: { opacity: 0, y: -16 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -16 },
},
tableRow: {
initial: { opacity: 0, y: 4 },
animate: { opacity: 1, y: 0 },
},
cardItem: {
initial: { opacity: 0, y: 12, scale: 0.98 },
animate: { opacity: 1, y: 0, scale: 1 },
},
sidebarSlide: {
initial: { opacity: 0, x: -8 },
animate: { opacity: 1, x: 0 },
exit: { opacity: 0, x: -8 },
},
} as const
export const STAGGER_VARIANTS: Variants = {
initial: {},
animate: { transition: { staggerChildren: 0.04 } },
}
export const STAGGER_ITEM_VARIANTS: Variants = {
initial: { opacity: 0, y: 8 },
animate: { opacity: 1, y: 0, transition: MOTION_TRANSITION.default },
}
export const TABLE_STAGGER_VARIANTS: Variants = {
initial: {},
animate: { transition: { staggerChildren: 0.03 } },
}
export const TABLE_ROW_VARIANTS: Variants = {
initial: { opacity: 0, y: 4 },
animate: { opacity: 1, y: 0, transition: MOTION_TRANSITION.fast },
}
export const CARD_STAGGER_VARIANTS: Variants = {
initial: {},
animate: { transition: { staggerChildren: 0.05 } },
}
export const CARD_ITEM_VARIANTS: Variants = {
initial: { opacity: 0, y: 12, scale: 0.98 },
animate: {
opacity: 1,
y: 0,
scale: 1,
transition: MOTION_TRANSITION.default,
},
}
export const SIDEBAR_STAGGER_VARIANTS: Variants = {
initial: {},
animate: { transition: { staggerChildren: 0.03, delayChildren: 0.05 } },
}
export const SIDEBAR_ITEM_VARIANTS: Variants = {
initial: { opacity: 0, x: -8 },
animate: { opacity: 1, x: 0, transition: MOTION_TRANSITION.fast },
}
+3 -6
View File
@@ -37,7 +37,6 @@ import { handleServerError } from '@/lib/handle-server-error'
import { useAuthStore } from '@/stores/auth-store'
import { DirectionProvider } from './context/direction-provider'
import { FontProvider } from './context/font-provider'
import { ThemeProvider } from './context/theme-provider'
import './i18n/config'
// Generated Routes
@@ -165,11 +164,9 @@ if (!rootElement.innerHTML) {
<StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<FontProvider>
<DirectionProvider>
<RouterProvider router={router} />
</DirectionProvider>
</FontProvider>
<DirectionProvider>
<RouterProvider router={router} />
</DirectionProvider>
</ThemeProvider>
</QueryClientProvider>
</StrictMode>
+7 -75
View File
@@ -396,8 +396,7 @@ For commercial licensing, please contact support@quantumnous.com
.animate-appear,
.animate-appear-zoom,
.animate-scroll-up,
.animate-scroll-down,
[data-slot='table'] tbody tr {
.animate-scroll-down {
animation: none !important;
opacity: 1;
transform: none;
@@ -482,79 +481,12 @@ For commercial licensing, please contact support@quantumnous.com
}
}
/* Micro-interactions — Vercel-style subtle hover/active feedback */
@media (prefers-reduced-motion: no-preference) {
[data-slot='card']:not([data-card-hover='false']) {
transition:
transform 150ms ease,
box-shadow 150ms ease;
}
@media (min-width: 641px) {
[data-slot='card']:not([data-card-hover='false']):hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.06);
}
.dark [data-slot='card']:not([data-card-hover='false']):hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 0.3);
}
}
button:not(:disabled):active,
[role='button']:not(:disabled):active {
transform: scale(0.98);
}
[data-slot='table'] tr {
transition: background-color 120ms ease;
}
/* Table row stagger-in animation */
[data-slot='table'] tbody tr {
animation: tableRowEnter 0.2s cubic-bezier(0.33, 1, 0.68, 1) both;
}
[data-slot='table'] tbody tr:nth-child(2) {
animation-delay: 25ms;
}
[data-slot='table'] tbody tr:nth-child(3) {
animation-delay: 50ms;
}
[data-slot='table'] tbody tr:nth-child(4) {
animation-delay: 75ms;
}
[data-slot='table'] tbody tr:nth-child(5) {
animation-delay: 100ms;
}
[data-slot='table'] tbody tr:nth-child(6) {
animation-delay: 125ms;
}
[data-slot='table'] tbody tr:nth-child(7) {
animation-delay: 150ms;
}
[data-slot='table'] tbody tr:nth-child(8) {
animation-delay: 175ms;
}
[data-slot='table'] tbody tr:nth-child(9) {
animation-delay: 200ms;
}
[data-slot='table'] tbody tr:nth-child(10) {
animation-delay: 225ms;
}
[data-slot='table'] tbody tr:nth-child(n + 11) {
animation-delay: 250ms;
}
}
@keyframes tableRowEnter {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
/* Micro-interactions admin surfaces stay calm: color transitions only.
* No entrance animations, no hover lift/shadow, no press scaling. High-
* frequency workflows (scanning tables, toggling filters) read as instant;
* decorative motion belongs to the landing page (landing-* utilities). */
[data-slot='table'] tr {
transition: background-color 120ms ease;
}
/* Tabular numbers for data-heavy contexts */
+2 -2
View File
@@ -419,8 +419,8 @@ For commercial licensing, please contact support@quantumnous.com
* cards/borders rather than tinting them with the clay accent).
* - `simple-large`: keeps intentionally neutral, high-contrast surfaces. */
[data-theme-preset]:not([data-theme-preset='default']):not(
[data-theme-preset='anthropic']
):not([data-theme-preset='simple-large']) {
[data-theme-preset='anthropic']
):not([data-theme-preset='simple-large']) {
--card: color-mix(in oklch, var(--primary) 3%, var(--background));
--popover: color-mix(in oklch, var(--primary) 5%, var(--background));
--muted: color-mix(in oklch, var(--primary) 7%, var(--background));
+16 -8
View File
@@ -19,7 +19,17 @@ For commercial licensing, please contact support@quantumnous.com
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-sans: 'Public Sans', sans-serif;
/* Primary UI face. `@fontsource-variable/public-sans` registers the family
* as 'Public Sans Variable' (NOT 'Public Sans'), so that name must lead the
* stack or the webfont silently never applies. Public Sans carries no CJK
* glyphs, so explicit CJK UI fonts (PingFang / YaHei UI / Noto Sans CJK)
* sit BEFORE the western system fallbacks otherwise Windows browsers pick
* a legacy serif-ish CJK default and Latin/CJK baselines drift apart. */
--font-sans:
'Public Sans Variable', 'Public Sans', -apple-system, BlinkMacSystemFont,
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei',
'Noto Sans SC', 'Noto Sans CJK SC', 'Segoe UI', 'Helvetica Neue', Arial,
sans-serif;
/* Editorial serif token the body face used by the `serif` font axis
* and by presets that opt in (currently Anthropic). Lora carries only
* Latin glyphs, so the stack walks through CJK serifs (Noto / Source Han
@@ -33,12 +43,6 @@ For commercial licensing, please contact support@quantumnous.com
'Songti SC', 'STSong', 'STSongti-SC-Regular', 'PingFang SC', 'SimSun',
'NSimSun', '宋体', 'FangSong', '仿宋', 'KaiTi', '楷体', Georgia,
'Times New Roman', Cambria, 'Liberation Serif', serif;
--font-inter:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
--font-manrope:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
--color-background: var(--background);
--color-foreground: var(--foreground);
@@ -91,7 +95,11 @@ For commercial licensing, please contact support@quantumnous.com
}
:root {
--radius: 1rem;
/* Single radius source. 0.625rem (10px) is the shadcn default for admin
* density; larger values ripple through the derived scale above and make
* data-dense tables/cards read "bubbly". User can still pick rounder axes
* via data-theme-radius. */
--radius: 0.625rem;
--app-header-height: 3rem;
/* Static build-channel fallback consumed when JS hasn't booted yet. */
--app-rev: '2k6e8r7p';