mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-07 01:56:53 +00:00
* 🎨 feat(web/default): add shadcn-style theme presets, radius prefs, and fix selection badges Integrate the qn-platform–style OKLCH color system into the default frontend while keeping the existing blue-tinted dark tokens for the default theme. Add [data-theme-preset] palettes for seven named presets plus the default zinc-like scale, define [data-theme-radius] overrides so user radius beats preset --radius, and align the Tailwind @custom-variant dark helper with .dark usage. Introduce ThemeCustomizationProvider to own preset and radius state, persist choices in cookies (theme-preset, theme-radius), and sync data-theme-preset / data-theme-radius on <html>. Wrap the tree in main.tsx. Extend ConfigDrawer with theme preset swatches (scoped data-theme-preset) and radius previews wired to context; refactor swatch/card markup so selected CircleCheck badges sit outside clipped rows (remove outer overflow-hidden that hid the centered checkmark). Add i18n keys for preset names, radius, and accessibility labels across en, zh, fr, ja, ru, vi. * 🎨 fix(web): align segmented controls with theme radius tokens - Replace hard-coded inner pill radii (rounded-[5px]) on dashboard chart toolbars with radius-md so the active state follows --radius when users change Radius in Theme Settings. - Use nested radii consistent with TabsList/TabsTrigger: outer rounded-lg (var(--radius)) and inner rounded-md (calc(var(--radius) - 2px)) so the track and active thumb stay concentric at small scales (e.g. 0.3rem) instead of a squared “focus” block inside a rounded shell. - Apply the same pattern to pricing SegmentedControl and the segmented groups in consumption-distribution-chart, model-charts, and user-charts. Verified: bun run typecheck (web/default) * ✨ feat(pricing): enrich model details with uptime sparkline and API documentation Add a compact 30-day uptime sparkline (OpenRouter-style bars + aggregate %) with per-day tooltips, surface it in a status row under quick stats and in the per-group performance table, and extend mock data so uptime series are stable and optionally scoped by group. Introduce an API tab with Shiki-highlighted code samples (cURL, Python, TypeScript, JavaScript), endpoint-type switching, authentication guidance, a supported-parameters table, and mock per-group RPM/TPM/RPD limits. Infer vendor, tokenizer, license, and data-retention hints for a provider & data privacy card on the Overview tab (capabilities/modalities stay with model identity; rate limits stay with the API tab). Update i18n for all new user-facing strings across en, zh, fr, ja, ru, and vi. * 🏆 feat(rankings): add comprehensive rankings dashboard Add a mock-data powered rankings experience with period tabs, model, app, and vendor leaderboards, market share and history charts, movers, new releases, and per-category sections while backend analytics are pending. Link ranked models to pricing details and ranked vendors to filtered pricing results, and include localized copy for all supported frontend locales. * fix(theme): correct theme preset selection state - update Base UI Radio selectors to use data-checked/data-unchecked states. - fix unchecked theme options still showing selected indicators. - isolate the default theme preview tokens to prevent preset changes from leaking into it. * fix(setup): correct usage mode radio state - use Base UI data-checked/data-unchecked states for RadioGroup styling. - hide radio indicators when options are unchecked to avoid setup page display issues. - drive usage mode card and icon selection styles from Base UI state. * fix(auth): submit sign-in and sign-up forms * 🎨 refactor: Align default theme with shadcn Base Nova and prune legacy customization Migrate shadcn UI to Base UI primitives via CLI (`base-nova` / `components.json`) and reinstall full component registry with `--overwrite`, including Hugeicons-backed widgets and newly added registry components. - Remove custom multi-preset/theme-radius system (`ThemeCustomizationProvider`, cookies, preset UI from config drawer); rely on official semantic CSS tokens + light/dark only. - Replace `theme.css` with shadcn’s documented neutral `:root`/`.dark` palette and `@theme inline` mappings (plus skeleton token vars for existing shimmer usage). - Update global styles for Base UI: collapsible animation uses `--collapsible-panel-height`; clarify scroll-lock override comment. Application compatibility: - Keep minimal shims where app code diverged from official APIs (popover collision props, combobox legacy `options` callers, Spinner prop typing). - Switch interactive styling from Radix-era `data-state` / `--radix-*` selectors to Base UI semantics (`data-open`, `data-popup-open`, `data-panel-open`, `--anchor-width`, etc.) Tooling / docs / build: - Rename Rsbuild vendor chunk grouping to `@base-ui` + transitive `@radix-ui`. - Refresh AGENTS.md / CLAUDE.md / classic→default sync skill for Base UI stack. - Bump `package.json` / lockfile for shadcn-postinstall deps (Hugeicons, chart stack, themes, etc.) Verified: `bun run typecheck` passes. Note: `bun run lint` still reports pre-existing hooks rule violations elsewhere; not addressed in this change. * 🎨 chore(web/default): unify table toolbar, relocate usage stats, refine filters - Refactor DataTableToolbar to a single wrapping flex row with a right-aligned action cluster (Reset / Search / View / Expand) for a cleaner Ant Design Pro–style filter bar; remove the dedicated stats row and the toolbar `stats` prop. - Move Common Logs summary badges (Usage / RPM / TPM) and the sensitive- data visibility toggle into the page header via CommonLogsHeaderActions and SectionPageLayout.Actions so the toolbar stays focused on filters. - Slim CommonLogsFilterBar props (no stats / preActions eye control). - Improve CompactDateTimeRangePicker: show minute-precision labels on the trigger (seconds omitted; aligns with datetime-local inputs); widen the trigger on sm+ breakpoints so the full range is visible without truncation; apply the same width in task logs filters. - Simplify DataTableViewOptions: text-only “View” trigger, no sliders icon. - Earlier layout tweak: extra top padding on SectionPageLayout scroll content so control focus rings are not clipped by overflow. * feat(web/default): Base UI migration and component foundation Migrate from Radix UI to Base UI, rewrite core UI primitives, update dependencies (recharts, date-fns, next-themes), add shadcn agent skill documentation, and refresh AI element components. This is the foundational work from the v2/localmain lineage that was not covered by the individual feature commits above. --------- Co-authored-by: t0ng7u <dev@aiass.cc> Co-authored-by: QuentinHsu <xuquentinyang@gmail.com>
526 lines
15 KiB
TypeScript
Vendored
526 lines
15 KiB
TypeScript
Vendored
import { useState, useEffect, useRef, type ReactNode } from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
type AccentTone = 'emerald' | 'amber' | 'blue' | 'violet'
|
|
|
|
interface ApiDemoConfig {
|
|
id: string
|
|
label: string
|
|
method: 'POST' | 'GET'
|
|
endpoint: string
|
|
headers: string[]
|
|
request: string[]
|
|
response: string[]
|
|
responseHighlights: string[]
|
|
tokens: number
|
|
latency: number
|
|
accent: AccentTone
|
|
}
|
|
|
|
const ACCENT_CLASSES: Record<
|
|
AccentTone,
|
|
{
|
|
activeText: string
|
|
activeBorder: string
|
|
badge: string
|
|
}
|
|
> = {
|
|
emerald: {
|
|
activeText: 'text-emerald-600 dark:text-emerald-400',
|
|
activeBorder: 'border-emerald-500 dark:border-emerald-400',
|
|
badge:
|
|
'bg-emerald-500/10 text-emerald-600 dark:bg-emerald-400/10 dark:text-emerald-400',
|
|
},
|
|
amber: {
|
|
activeText: 'text-amber-600 dark:text-amber-400',
|
|
activeBorder: 'border-amber-500 dark:border-amber-400',
|
|
badge:
|
|
'bg-amber-500/10 text-amber-600 dark:bg-amber-400/10 dark:text-amber-400',
|
|
},
|
|
blue: {
|
|
activeText: 'text-blue-600 dark:text-blue-400',
|
|
activeBorder: 'border-blue-500 dark:border-blue-400',
|
|
badge:
|
|
'bg-blue-500/10 text-blue-600 dark:bg-blue-400/10 dark:text-blue-400',
|
|
},
|
|
violet: {
|
|
activeText: 'text-violet-600 dark:text-violet-400',
|
|
activeBorder: 'border-violet-500 dark:border-violet-400',
|
|
badge:
|
|
'bg-violet-500/10 text-violet-600 dark:bg-violet-400/10 dark:text-violet-400',
|
|
},
|
|
}
|
|
|
|
const API_DEMOS: ApiDemoConfig[] = [
|
|
{
|
|
id: 'gpt-chat',
|
|
label: 'Chat',
|
|
method: 'POST',
|
|
endpoint: '/v1/chat/completions',
|
|
headers: ['"Authorization: Bearer sk-••••"'],
|
|
request: [
|
|
'"model": "your-model",',
|
|
'"messages": [',
|
|
' { "role": "user", "content": "..." }',
|
|
']',
|
|
],
|
|
response: [
|
|
'{',
|
|
' "choices": [{ "message": { "content": <text> } }],',
|
|
' "usage": { "total_tokens": <tokens> }',
|
|
'}',
|
|
],
|
|
responseHighlights: ['<text>', '<tokens>'],
|
|
tokens: 27,
|
|
latency: 142,
|
|
accent: 'emerald',
|
|
},
|
|
{
|
|
id: 'responses',
|
|
label: 'Responses',
|
|
method: 'POST',
|
|
endpoint: '/v1/responses',
|
|
headers: ['"Authorization: Bearer sk-••••"'],
|
|
request: ['"model": "your-model",', '"input": "..."'],
|
|
response: [
|
|
'{',
|
|
' "output": [{ "type": "output_text", "text": <text> }],',
|
|
' "usage": { "total_tokens": <tokens> }',
|
|
'}',
|
|
],
|
|
responseHighlights: ['<text>', '<tokens>'],
|
|
tokens: 31,
|
|
latency: 168,
|
|
accent: 'amber',
|
|
},
|
|
{
|
|
id: 'claude',
|
|
label: 'Claude',
|
|
method: 'POST',
|
|
endpoint: '/v1/messages',
|
|
headers: ['"x-api-key: sk-••••"', '"anthropic-version: 2023-06-01"'],
|
|
request: [
|
|
'"model": "your-model",',
|
|
'"max_tokens": 1024,',
|
|
'"messages": [',
|
|
' { "role": "user", "content": "..." }',
|
|
']',
|
|
],
|
|
response: [
|
|
'{',
|
|
' "content": [{ "type": "text", "text": <text> }],',
|
|
' "usage": { "input_tokens": <in>, "output_tokens": <out> }',
|
|
'}',
|
|
],
|
|
responseHighlights: ['<text>', '<in>', '<out>'],
|
|
tokens: 29,
|
|
latency: 156,
|
|
accent: 'blue',
|
|
},
|
|
{
|
|
id: 'gemini',
|
|
label: 'Gemini',
|
|
method: 'POST',
|
|
endpoint: '/v1beta/models/{model}:generateContent',
|
|
headers: ['"x-goog-api-key: sk-••••"'],
|
|
request: [
|
|
'"contents": [',
|
|
' { "role": "user",',
|
|
' "parts": [{ "text": "..." }] }',
|
|
']',
|
|
],
|
|
response: [
|
|
'{',
|
|
' "candidates": [{ "content": { "parts": [{ "text": <text> }] } }],',
|
|
' "usageMetadata": { "totalTokenCount": <tokens> }',
|
|
'}',
|
|
],
|
|
responseHighlights: ['<text>', '<tokens>'],
|
|
tokens: 25,
|
|
latency: 93,
|
|
accent: 'violet',
|
|
},
|
|
]
|
|
|
|
const CYCLE_INTERVAL = 4500
|
|
const TRANSITION_MS = 220
|
|
|
|
export function HeroTerminalDemo() {
|
|
const [activeIndex, setActiveIndex] = useState(0)
|
|
const [transitioning, setTransitioning] = useState(false)
|
|
const intervalRef = useRef<ReturnType<typeof setInterval>>(undefined)
|
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
|
|
useEffect(() => {
|
|
const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
if (mq.matches) return
|
|
|
|
intervalRef.current = setInterval(() => {
|
|
setTransitioning(true)
|
|
timeoutRef.current = setTimeout(() => {
|
|
setActiveIndex((prev) => (prev + 1) % API_DEMOS.length)
|
|
setTransitioning(false)
|
|
}, TRANSITION_MS)
|
|
}, CYCLE_INTERVAL)
|
|
|
|
return () => {
|
|
if (intervalRef.current) clearInterval(intervalRef.current)
|
|
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
|
}
|
|
}, [])
|
|
|
|
const handleSelect = (index: number) => {
|
|
if (index === activeIndex) return
|
|
if (intervalRef.current) clearInterval(intervalRef.current)
|
|
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
|
setTransitioning(true)
|
|
timeoutRef.current = setTimeout(() => {
|
|
setActiveIndex(index)
|
|
setTransitioning(false)
|
|
}, TRANSITION_MS)
|
|
}
|
|
|
|
const demo = API_DEMOS[activeIndex]
|
|
const accent = ACCENT_CLASSES[demo.accent]
|
|
|
|
return (
|
|
<div className='mx-auto mt-16 w-full max-w-2xl'>
|
|
<div
|
|
className={cn(
|
|
'overflow-hidden rounded-2xl border backdrop-blur-sm',
|
|
'border-border/60 bg-white/95 shadow-[0_20px_50px_-25px_rgba(15,23,42,0.18)]',
|
|
'dark:border-white/[0.06] dark:bg-[#0b0f17]/95 dark:shadow-[0_20px_60px_-25px_rgba(0,0,0,0.7)]'
|
|
)}
|
|
>
|
|
{/* Tab strip */}
|
|
<div
|
|
className={cn(
|
|
'flex items-center gap-1 border-b px-2 sm:gap-1.5 sm:px-3',
|
|
'border-border/50 dark:border-white/[0.05]'
|
|
)}
|
|
>
|
|
{API_DEMOS.map((item, index) => {
|
|
const tone = ACCENT_CLASSES[item.accent]
|
|
const isActive = index === activeIndex
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => handleSelect(index)}
|
|
className={cn(
|
|
'relative -mb-px flex items-center gap-1.5 border-b-2 px-2.5 py-2.5 text-[11px] font-medium tracking-wide transition-colors sm:px-3 sm:text-xs',
|
|
isActive
|
|
? `${tone.activeBorder} ${tone.activeText}`
|
|
: 'text-foreground/40 hover:text-foreground/70 border-transparent'
|
|
)}
|
|
>
|
|
{item.label}
|
|
</button>
|
|
)
|
|
})}
|
|
<div className='ml-auto flex items-center gap-2 pr-2 sm:pr-3'>
|
|
<span className='inline-block size-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.45)]' />
|
|
<span className='text-foreground/40 font-mono text-[10px] tracking-wider uppercase'>
|
|
200 ok
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Endpoint row */}
|
|
<div
|
|
className={cn(
|
|
'flex items-center gap-2.5 border-b px-5 py-3',
|
|
'border-border/40 dark:border-white/[0.04]'
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
'rounded-md px-1.5 py-0.5 font-mono text-[10px] font-semibold tracking-wider',
|
|
accent.badge
|
|
)}
|
|
>
|
|
{demo.method}
|
|
</span>
|
|
<code
|
|
className={cn(
|
|
'text-foreground/75 truncate font-mono text-[12.5px] transition-opacity duration-200',
|
|
transitioning ? 'opacity-0' : 'opacity-100'
|
|
)}
|
|
>
|
|
{demo.endpoint}
|
|
</code>
|
|
</div>
|
|
|
|
{/* Body — fixed rows so neither block shifts when switching demos */}
|
|
<div className='grid h-[400px] grid-rows-[235px_minmax(0,1fr)] font-mono text-[12.5px] leading-[1.55]'>
|
|
{/* Request */}
|
|
<RequestBlock demo={demo} transitioning={transitioning} />
|
|
|
|
{/* Response */}
|
|
<ResponseBlock demo={demo} transitioning={transitioning} />
|
|
</div>
|
|
|
|
{/* Footer metrics */}
|
|
<div
|
|
className={cn(
|
|
'flex items-center justify-between border-t px-5 py-2.5',
|
|
'border-border/40 bg-muted/30 dark:border-white/[0.05] dark:bg-white/[0.02]'
|
|
)}
|
|
>
|
|
<div className='text-foreground/40 flex items-center gap-3 text-[10px] tabular-nums'>
|
|
<span className='flex items-center gap-1'>
|
|
<span className='font-mono'>{demo.latency}</span>
|
|
<span className='tracking-wider uppercase'>ms</span>
|
|
</span>
|
|
<span className='bg-foreground/15 size-1 rounded-full' />
|
|
<span className='flex items-center gap-1'>
|
|
<span className='font-mono'>{demo.tokens}</span>
|
|
<span className='tracking-wider uppercase'>tokens</span>
|
|
</span>
|
|
<span className='bg-foreground/15 size-1 rounded-full' />
|
|
<span className='flex items-center gap-1'>
|
|
<span className='tracking-wider uppercase'>cost</span>
|
|
<span className='font-mono'>
|
|
${(demo.tokens * 0.00003).toFixed(5)}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
<span className='text-foreground/30 font-mono text-[10px] tracking-wider uppercase'>
|
|
stream · sse
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function RequestBlock(props: { demo: ApiDemoConfig; transitioning: boolean }) {
|
|
const { demo, transitioning } = props
|
|
|
|
return (
|
|
<div className='relative px-5 py-4'>
|
|
<SectionLabel>Request</SectionLabel>
|
|
<div
|
|
className={cn(
|
|
'mt-2 transition-opacity duration-200',
|
|
transitioning ? 'opacity-0' : 'opacity-100'
|
|
)}
|
|
>
|
|
<CodeLine>
|
|
<Command>curl</Command> <Flag>-X</Flag> <Flag>POST</Flag>{' '}
|
|
<StringText>"{demo.endpoint}"</StringText>{' '}
|
|
<Muted>{'\\'}</Muted>
|
|
</CodeLine>
|
|
{demo.headers.map((header) => (
|
|
<CodeLine key={header} indent={2}>
|
|
<Flag>-H</Flag> <StringText>{header}</StringText>{' '}
|
|
<Muted>{'\\'}</Muted>
|
|
</CodeLine>
|
|
))}
|
|
<CodeLine indent={2}>
|
|
<Flag>-d</Flag> <StringText>'{'{'}</StringText>
|
|
</CodeLine>
|
|
{demo.request.map((line, i) => (
|
|
<CodeLine key={i} indent={4}>
|
|
{renderJsonLine(line)}
|
|
</CodeLine>
|
|
))}
|
|
<CodeLine indent={2}>
|
|
<StringText>{'}'}'</StringText>
|
|
</CodeLine>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ResponseBlock(props: { demo: ApiDemoConfig; transitioning: boolean }) {
|
|
const { demo, transitioning } = props
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'relative border-t px-5 py-4',
|
|
'border-border/40 bg-muted/20 dark:border-white/[0.05] dark:bg-white/[0.015]'
|
|
)}
|
|
>
|
|
<SectionLabel>Response</SectionLabel>
|
|
<div
|
|
className={cn(
|
|
'mt-2 transition-opacity duration-200',
|
|
transitioning ? 'opacity-0' : 'opacity-100'
|
|
)}
|
|
>
|
|
{demo.response.map((line, i) => (
|
|
<CodeLine key={i}>{renderResponseLine(line, demo)}</CodeLine>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SectionLabel(props: { children: ReactNode }) {
|
|
return (
|
|
<span className='text-foreground/30 font-sans text-[10px] font-semibold tracking-[0.18em] uppercase'>
|
|
{props.children}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
const STRING_RE = /"[^"]*"/g
|
|
const PLACEHOLDER_RE = /<[a-z]+>/gi
|
|
|
|
function renderJsonLine(line: string): ReactNode {
|
|
if (!line.trim()) return <Muted> </Muted>
|
|
return tokenize(line)
|
|
}
|
|
|
|
function renderResponseLine(line: string, demo: ApiDemoConfig): ReactNode {
|
|
if (!line.trim()) return <Muted> </Muted>
|
|
|
|
const segments: ReactNode[] = []
|
|
let cursor = 0
|
|
const matches = [...line.matchAll(PLACEHOLDER_RE)]
|
|
|
|
if (matches.length === 0) return tokenize(line)
|
|
|
|
matches.forEach((match, idx) => {
|
|
const start = match.index ?? 0
|
|
if (start > cursor) {
|
|
segments.push(
|
|
<span key={`pre-${idx}`}>{tokenize(line.slice(cursor, start))}</span>
|
|
)
|
|
}
|
|
const placeholder = match[0]
|
|
if (placeholder === '<text>') {
|
|
segments.push(
|
|
<Accent key={`ph-${idx}`} accent={demo.accent}>
|
|
{`"${truncateResponse(demo)}"`}
|
|
</Accent>
|
|
)
|
|
} else if (placeholder === '<tokens>') {
|
|
segments.push(<NumberText key={`ph-${idx}`}>{demo.tokens}</NumberText>)
|
|
} else if (placeholder === '<in>') {
|
|
segments.push(
|
|
<NumberText key={`ph-${idx}`}>
|
|
{Math.floor(demo.tokens * 0.4)}
|
|
</NumberText>
|
|
)
|
|
} else if (placeholder === '<out>') {
|
|
segments.push(
|
|
<NumberText key={`ph-${idx}`}>
|
|
{Math.ceil(demo.tokens * 0.6)}
|
|
</NumberText>
|
|
)
|
|
} else {
|
|
segments.push(<Muted key={`ph-${idx}`}>{placeholder}</Muted>)
|
|
}
|
|
cursor = start + placeholder.length
|
|
})
|
|
|
|
if (cursor < line.length) {
|
|
segments.push(<span key='tail'>{tokenize(line.slice(cursor))}</span>)
|
|
}
|
|
|
|
return segments
|
|
}
|
|
|
|
function truncateResponse(demo: ApiDemoConfig): string {
|
|
const map: Record<string, string> = {
|
|
'gpt-chat': 'Chat request routed.',
|
|
responses: 'Response workflow ready.',
|
|
claude: 'Claude message routed.',
|
|
gemini: 'Gemini request served.',
|
|
}
|
|
return map[demo.id] ?? '...'
|
|
}
|
|
|
|
function tokenize(input: string): ReactNode {
|
|
// Split string into "..." string runs and the rest, then color keys/punct.
|
|
const segments: ReactNode[] = []
|
|
let cursor = 0
|
|
const matches = [...input.matchAll(STRING_RE)]
|
|
|
|
matches.forEach((match, idx) => {
|
|
const start = match.index ?? 0
|
|
if (start > cursor) {
|
|
segments.push(
|
|
<Muted key={`m-${idx}`}>{input.slice(cursor, start)}</Muted>
|
|
)
|
|
}
|
|
const text = match[0]
|
|
const after = input.slice(start + text.length).trimStart()
|
|
const isKey = after.startsWith(':')
|
|
if (isKey) {
|
|
segments.push(<Key key={`k-${idx}`}>{text}</Key>)
|
|
} else {
|
|
segments.push(<StringText key={`s-${idx}`}>{text}</StringText>)
|
|
}
|
|
cursor = start + text.length
|
|
})
|
|
|
|
if (cursor < input.length) {
|
|
segments.push(<Muted key='tail'>{input.slice(cursor)}</Muted>)
|
|
}
|
|
|
|
return segments
|
|
}
|
|
|
|
function CodeLine(props: { children: ReactNode; indent?: number }) {
|
|
return (
|
|
<div className='break-words whitespace-pre-wrap'>
|
|
{props.indent ? (
|
|
<span
|
|
aria-hidden
|
|
className='inline-block'
|
|
style={{ width: `${props.indent}ch` }}
|
|
/>
|
|
) : null}
|
|
{props.children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Command(props: { children: ReactNode }) {
|
|
return (
|
|
<span className='font-medium text-emerald-600 dark:text-emerald-400'>
|
|
{props.children}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function Flag(props: { children: ReactNode }) {
|
|
return (
|
|
<span className='text-blue-600 dark:text-blue-400'>{props.children}</span>
|
|
)
|
|
}
|
|
|
|
function Key(props: { children: ReactNode }) {
|
|
return (
|
|
<span className='text-sky-700 dark:text-sky-300'>{props.children}</span>
|
|
)
|
|
}
|
|
|
|
function StringText(props: { children: ReactNode }) {
|
|
return (
|
|
<span className='text-amber-700 dark:text-amber-300'>{props.children}</span>
|
|
)
|
|
}
|
|
|
|
function NumberText(props: { children: ReactNode }) {
|
|
return (
|
|
<span className='font-medium text-violet-600 dark:text-violet-300'>
|
|
{props.children}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function Muted(props: { children: ReactNode }) {
|
|
return <span className='text-foreground/55'>{props.children}</span>
|
|
}
|
|
|
|
function Accent(props: { children: ReactNode; accent: AccentTone }) {
|
|
const tone = ACCENT_CLASSES[props.accent]
|
|
return (
|
|
<span className={cn('font-medium', tone.activeText)}>{props.children}</span>
|
|
)
|
|
}
|