✨ 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:
+1
-1
@@ -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'>
|
||||
|
||||
+2
-3
@@ -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
@@ -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
@@ -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
@@ -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 ? (
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user