From 6bbddb104637d0871dac807aad66d8f8e358ccd0 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 11 Jul 2026 15:32:35 +0800 Subject: [PATCH 1/3] feat(timing): add timing metrics display for stream logs and enhance localization --- .../overview/overview-dashboard.tsx | 6 +- .../components/overview/summary-cards.tsx | 30 ++-- .../dashboard/components/ui/stat-card.tsx | 130 ++++++++++------ .../pricing/components/model-card.tsx | 7 +- .../columns/common-logs-columns.tsx | 130 ++++------------ .../components/timing-metrics-cell.tsx | 146 ++++++++++++++++++ .../components/usage-logs-mobile-card.tsx | 9 +- web/default/src/i18n/locales/en.json | 2 + web/default/src/i18n/locales/fr.json | 2 + web/default/src/i18n/locales/ja.json | 2 + web/default/src/i18n/locales/ru.json | 2 + web/default/src/i18n/locales/vi.json | 2 + web/default/src/i18n/locales/zh-TW.json | 4 +- web/default/src/i18n/locales/zh.json | 4 +- web/default/src/styles/theme-presets.css | 9 ++ web/default/src/styles/theme.css | 8 +- 16 files changed, 317 insertions(+), 176 deletions(-) create mode 100644 web/default/src/features/usage-logs/components/timing-metrics-cell.tsx diff --git a/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx b/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx index 6f09432962..c5909ff67e 100644 --- a/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx +++ b/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx @@ -183,7 +183,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) { <>
- {previewLines.map((line, index) => ( + {previewLines.map((line) => ( diff --git a/web/default/src/features/dashboard/components/overview/summary-cards.tsx b/web/default/src/features/dashboard/components/overview/summary-cards.tsx index 6970b68108..c714ccc8fe 100644 --- a/web/default/src/features/dashboard/components/overview/summary-cards.tsx +++ b/web/default/src/features/dashboard/components/overview/summary-cards.tsx @@ -211,6 +211,20 @@ export function SummaryCards() { const runwayDays = getRunwayDays(remainQuota, recentUsage) const todayUsageDisplay = formatQuota(recentUsage) + let runwayDisplay: string + if (runwayDays !== null) { + if (runwayDays < 1) { + runwayDisplay = t('Less than 1 day left') + } else if (runwayDays > 999) { + runwayDisplay = `999+ ${t('days')}` + } else { + runwayDisplay = `~${formatNumber(Math.floor(runwayDays))} ${t('days')}` + } + } else if (remainQuota <= 0) { + runwayDisplay = t('Balance depleted') + } else { + runwayDisplay = t('No recent usage') + } const items = useSummaryCardsConfig({ ...summaryValues, @@ -218,7 +232,7 @@ export function SummaryCards() { currencyEnabled, currencyLabel, }).map((config, index) => { - const tones = ['rose', 'teal', 'gray'] as const + const tones = ['accent-1', 'accent-2', 'accent-3'] as const return { key: config.key, @@ -226,7 +240,7 @@ export function SummaryCards() { value: config.value, desc: config.description, icon: config.icon, - tone: tones[index] ?? 'gray', + tone: tones[index] ?? 'accent-3', sparkline: config.key === 'todayUsage' ? sparklineData.usage @@ -270,7 +284,7 @@ export function SummaryCards() {
-
+
@@ -323,15 +337,7 @@ export function SummaryCards() { healthLevel === 'caution' && 'text-warning' )} > - {runwayDays !== null - ? runwayDays < 1 - ? t('Less than 1 day left') - : runwayDays > 999 - ? `999+ ${t('days')}` - : `~${formatNumber(Math.floor(runwayDays))} ${t('days')}` - : remainQuota <= 0 - ? t('Balance depleted') - : t('No recent usage')} + {runwayDisplay}
diff --git a/web/default/src/features/dashboard/components/ui/stat-card.tsx b/web/default/src/features/dashboard/components/ui/stat-card.tsx index 65b8e34687..e5eaea6a82 100644 --- a/web/default/src/features/dashboard/components/ui/stat-card.tsx +++ b/web/default/src/features/dashboard/components/ui/stat-card.tsx @@ -16,13 +16,13 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { type LucideIcon } from 'lucide-react' +import type { LucideIcon } from 'lucide-react' import { useId, type ReactNode } from 'react' import { Skeleton } from '@/components/ui/skeleton' import { cn } from '@/lib/utils' -type StatCardTone = 'rose' | 'teal' | 'gray' +type StatCardTone = 'accent-1' | 'accent-2' | 'accent-3' type StatCardSparklineVariant = 'bars' | 'line' type StatCardDetailTone = | 'default' @@ -52,15 +52,18 @@ interface StatCardProps { } const TONE_CLASSES: Record = { - 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', + 'accent-1': + 'from-overview-accent-1/80 via-overview-accent-1/45 to-overview-accent-1/5 dark:from-overview-accent-1/70 dark:via-overview-accent-1/30', + 'accent-2': + 'from-overview-accent-2/80 via-overview-accent-2/45 to-overview-accent-2/5 dark:from-overview-accent-2/70 dark:via-overview-accent-2/30', + 'accent-3': + 'from-overview-accent-3/80 via-overview-accent-3/45 to-overview-accent-3/5 dark:from-overview-accent-3/70 dark:via-overview-accent-3/30', } const LINE_TONE_CLASSES: Record = { - rose: 'text-warning', - teal: 'text-primary', - gray: 'text-muted-foreground', + 'accent-1': 'text-overview-accent-1', + 'accent-2': 'text-overview-accent-2', + 'accent-3': 'text-overview-accent-3', } const DETAIL_TONE_CLASSES: Record = { @@ -71,14 +74,24 @@ const DETAIL_TONE_CLASSES: Record = { destructive: 'text-destructive', } -function normalizeSparkline(values?: number[]): number[] { +interface SparklineBucket { + position: number + height: number +} + +function normalizeSparkline(values?: number[]): SparklineBucket[] { if (!values?.length) return [] const sanitized = values.map((value) => Math.max(0, Number(value) || 0)) const max = Math.max(...sanitized) - if (max <= 0) return sanitized.map(() => 0) + if (max <= 0) { + return sanitized.map((_, position) => ({ position, height: 0 })) + } - return sanitized.map((value) => Math.max(8, (value / max) * 100)) + return sanitized.map((value, position) => ({ + position, + height: Math.max(8, (value / max) * 100), + })) } function buildLineSparkline(values?: number[]) { @@ -97,7 +110,12 @@ function buildLineSparkline(values?: number[]) { sanitized.length === 1 ? width / 2 : (index / (sanitized.length - 1)) * width - const normalized = range > 0 ? (value - min) / range : max > 0 ? 0.5 : 0 + let normalized = 0 + if (range > 0) { + normalized = (value - min) / range + } else if (max > 0) { + normalized = 0.5 + } const y = height - padding - normalized * (height - padding * 2) return { x, y } @@ -106,8 +124,9 @@ function buildLineSparkline(values?: number[]) { const linePath = points .map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`) .join(' ') - const firstPoint = points[0] - const lastPoint = points[points.length - 1] + const firstPoint = points.at(0) + const lastPoint = points.at(-1) + if (!firstPoint || !lastPoint) return null const areaPath = `${linePath} L ${lastPoint.x} ${height} L ${firstPoint.x} ${height} Z` return { @@ -118,7 +137,7 @@ function buildLineSparkline(values?: number[]) { function LineSparkline(props: { values?: number[]; tone: StatCardTone }) { const rawGradientId = useId() - const gradientId = `stat-card-line-${rawGradientId.replace(/:/g, '')}` + const gradientId = `stat-card-line-${rawGradientId.replaceAll(':', '')}` const paths = buildLineSparkline(props.values) if (!paths) return ) } diff --git a/web/default/src/features/pricing/components/model-card.tsx b/web/default/src/features/pricing/components/model-card.tsx index 4e25eace52..24808763f2 100644 --- a/web/default/src/features/pricing/components/model-card.tsx +++ b/web/default/src/features/pricing/components/model-card.tsx @@ -116,14 +116,13 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { {entry.formatted} - /{tokenUnitLabel} ))} ) } else { priceSummary = ( - + {t('Dynamic Pricing')} ) @@ -144,7 +143,6 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { props.selectedGroup )} - /{tokenUnitLabel} {t('Output')}{' '} @@ -159,7 +157,6 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { props.selectedGroup )} - /{tokenUnitLabel} {hasCachedPrice && ( @@ -217,7 +214,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {

{props.model.model_name}

-
+
{priceSummary}
diff --git a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx index 404f02eb85..b1af4e02e6 100644 --- a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx +++ b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { type ColumnDef } from '@tanstack/react-table' -import { CircleAlert, GitBranch, Sparkles, KeyRound } from 'lucide-react' +import { GitBranch, Sparkles, KeyRound } from 'lucide-react' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -36,19 +36,13 @@ import { } from '@/components/ui/tooltip' import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar' import { formatBillingCurrencyFromUSD } from '@/lib/currency' -import { - formatUseTime, - formatLogQuota, - formatTimestampToDate, -} from '@/lib/format' +import { formatLogQuota, formatTimestampToDate } from '@/lib/format' import { cn } from '@/lib/utils' import { LOG_TYPE_ALL_VALUE } from '../../constants' import type { UsageLog } from '../../data/schema' import { formatModelName, - getFirstResponseTimeColor, - getResponseTimeColor, getTieredBillingSummary, hasAnyCacheTokens, parseLogOther, @@ -64,6 +58,7 @@ import { import type { LogOtherData } from '../../types' import { DetailsDialog } from '../dialogs/details-dialog' import { ModelBadge } from '../model-badge' +import { TimingMetricsCell, StreamTpsCell } from '../timing-metrics-cell' import { useUsageLogsContext } from '../usage-logs-provider' interface DetailSegment { @@ -619,113 +614,29 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { meta: { mobileTitle: true }, }, { - accessorKey: 'use_time', - header: t('Timing'), + accessorKey: 'is_stream', + header: t('Stream'), cell: ({ row }) => { const log = row.original if (!isTimingLogType(log.type)) return null const useTime = row.getValue('use_time') as number const other = parseLogOther(log.other) - const frt = other?.frt const tokensPerSecond = useTime > 0 && log.completion_tokens > 0 ? log.completion_tokens / useTime : null - const timeVariant = getResponseTimeColor(useTime, log.completion_tokens) - const frtVariant = frt - ? getFirstResponseTimeColor(frt / 1000) - : 'neutral' - - const timingBgMap: Record = { - 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', - } return ( -
-
- - {log.is_stream && - (frt != null && frt > 0 ? ( - - ) : ( - - ))} -
-
- - {log.is_stream ? t('Stream') : t('Non-stream')} - {tokensPerSecond != null && ( - <> - {' · '} - - {Math.round(tokensPerSecond)} - - {' t/s'} - - )} - - {log.is_stream && - other?.stream_status && - other.stream_status.status !== 'ok' && ( - - - } - > - -
-

- {t('Stream Status')}: {t('Error')} -

-

{other.stream_status.end_reason || 'unknown'}

- {(other.stream_status.error_count ?? 0) > 0 && ( -

- {t('Soft Errors')}:{' '} - {other.stream_status.error_count} -

- )} -
-
-
-
- )} -
-
+ ) }, + meta: { label: t('Stream') }, }, - { accessorKey: 'prompt_tokens', header: 'Tokens', @@ -773,6 +684,25 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { ) }, }, + { + accessorKey: 'use_time', + header: t('Timing'), + cell: ({ row }) => { + const log = row.original + if (!isTimingLogType(log.type)) return null + + const useTime = row.getValue('use_time') as number + const other = parseLogOther(log.other) + + return ( + + ) + }, + }, { accessorKey: 'quota', diff --git a/web/default/src/features/usage-logs/components/timing-metrics-cell.tsx b/web/default/src/features/usage-logs/components/timing-metrics-cell.tsx new file mode 100644 index 0000000000..409378183a --- /dev/null +++ b/web/default/src/features/usage-logs/components/timing-metrics-cell.tsx @@ -0,0 +1,146 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { CircleAlert } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip' +import { formatUseTime } from '@/lib/format' +import { cn } from '@/lib/utils' + +import type { LogOtherData } from '../types' + +interface TimingMetricsCellProps { + useTimeSec: number + frtMs?: number + isStream: boolean + className?: string +} + +export function TimingMetricsCell(props: TimingMetricsCellProps) { + const { t } = useTranslation() + const showFirstToken = props.isStream + const hasFrt = props.frtMs != null && props.frtMs > 0 + const firstTokenLabel = hasFrt + ? formatUseTime(props.frtMs! / 1000) + : t('N/A') + const totalTimeLabel = formatUseTime(props.useTimeSec) + + return ( +
+ +
+
+ + {t('First token')} + + + {showFirstToken ? firstTokenLabel : '—'} + +
+
+ + {t('Duration')} + + {totalTimeLabel} +
+
+
+ ) +} + +interface StreamTpsCellProps { + isStream: boolean + tokensPerSecond?: number | null + streamStatus?: LogOtherData['stream_status'] + className?: string +} + +export function StreamTpsCell(props: StreamTpsCellProps) { + const { t } = useTranslation() + const showStreamError = + props.isStream && + props.streamStatus && + props.streamStatus.status !== 'ok' + const tpsLabel = + props.tokensPerSecond != null + ? `${Math.round(props.tokensPerSecond)} t/s` + : '—' + + return ( +
+ + {props.isStream ? t('Stream') : t('Non-stream')} + {showStreamError && ( + + + } + /> + +
+

+ {t('Stream Status')}: {t('Error')} +

+

{props.streamStatus?.end_reason || 'unknown'}

+ {(props.streamStatus?.error_count ?? 0) > 0 && ( +

+ {t('Soft Errors')}: {props.streamStatus?.error_count} +

+ )} +
+
+
+
+ )} +
+ + {tpsLabel} + +
+ ) +} diff --git a/web/default/src/features/usage-logs/components/usage-logs-mobile-card.tsx b/web/default/src/features/usage-logs/components/usage-logs-mobile-card.tsx index 203c0ab21f..a9364fee8b 100644 --- a/web/default/src/features/usage-logs/components/usage-logs-mobile-card.tsx +++ b/web/default/src/features/usage-logs/components/usage-logs-mobile-card.tsx @@ -220,8 +220,8 @@ function CommonLogsCard({ valueClassName='[&_.flex-col]:max-w-none [&_.flex-col>*:not(:first-child)]:text-[11px] [&_.flex-col>*:not(:first-child)]:leading-none' /> ({ cell={cells.get('prompt_tokens')} primaryOnly /> + Date: Sat, 11 Jul 2026 19:25:04 +0800 Subject: [PATCH 2/3] feat: update theme colors --- web/default/src/components/config-drawer.tsx | 34 ++-- web/default/src/components/drawer-layout.ts | 9 +- web/default/src/components/ui/icon-badge.tsx | 85 ++++++++ web/default/src/components/ui/sidebar.tsx | 2 +- web/default/src/components/ui/table.tsx | 2 +- web/default/src/components/ui/titled-card.tsx | 12 +- .../dialogs/balance-query-dialog.tsx | 13 +- .../drawers/channel-mutate-drawer.tsx | 42 ++-- .../sections/channel-api-access-section.tsx | 1 + .../sections/channel-basic-section.tsx | 1 + .../sections/channel-models-section.tsx | 1 + .../dashboard/components/flow/flow-charts.tsx | 5 +- .../models/consumption-distribution-chart.tsx | 5 +- .../components/models/log-stat-cards.tsx | 81 +++++--- .../components/models/model-charts.tsx | 5 +- .../models/performance-overview.tsx | 25 +-- .../overview/announcements-panel.tsx | 5 +- .../components/overview/api-info-panel.tsx | 5 +- .../components/overview/faq-panel.tsx | 5 +- .../overview/overview-dashboard.tsx | 18 +- .../overview/performance-health-panel.tsx | 22 ++- .../components/overview/summary-cards.tsx | 17 +- .../components/overview/uptime-panel.tsx | 9 +- .../dashboard/components/ui/stat-card.tsx | 73 +++++-- .../components/users/user-charts.tsx | 5 +- .../dashboard/hooks/use-dashboard-config.tsx | 7 + web/default/src/features/dashboard/index.tsx | 56 ++++-- .../components/api-key-timestamp-cell.tsx | 68 +++++++ .../keys/components/api-keys-cells.tsx | 52 ++--- .../keys/components/api-keys-columns.tsx | 55 ++++-- .../components/api-keys-mutate-drawer.tsx | 16 +- .../keys/components/api-keys-table.tsx | 24 ++- .../components/dialogs/view-logs-dialog.tsx | 5 +- .../components/model-billing-mode-badge.tsx | 54 +++++ .../pricing/components/model-card.tsx | 22 +-- .../pricing/components/model-details.tsx | 61 +++--- .../pricing/components/pricing-columns.tsx | 19 +- .../pricing/components/pricing-sidebar.tsx | 2 +- .../components/checkin-calendar-card.tsx | 30 +-- .../components/language-preferences-card.tsx | 13 +- .../profile/components/passkey-card.tsx | 7 +- .../profile/components/profile-header.tsx | 20 +- .../components/profile-security-card.tsx | 18 +- .../components/profile-settings-card.tsx | 5 +- .../components/sidebar-modules-card.tsx | 7 +- .../profile/components/two-fa-card.tsx | 7 +- .../components/redemptions-mobile-list.tsx | 175 +++++++++++++++++ .../components/redemptions-table.tsx | 2 + .../subscriptions-mutate-drawer.tsx | 65 +++--- .../columns/common-logs-columns.tsx | 141 +++++++------ .../components/common-logs-filter-bar.tsx | 5 +- .../components/common-logs-stats.tsx | 5 +- .../dialogs/audio-preview-dialog.tsx | 5 +- .../components/dialogs/details-dialog.tsx | 48 +++-- .../components/logs-filter-toolbar.tsx | 32 ++- .../components/task-logs-filter-bar.tsx | 5 +- .../components/timing-metrics-cell.tsx | 153 +++++++++++---- .../components/usage-logs-mobile-card.tsx | 185 +++++++++++++++--- .../components/usage-logs-provider.tsx | 29 +++ .../components/usage-logs-table.tsx | 4 +- web/default/src/features/usage-logs/index.tsx | 22 +++ .../components/affiliate-rewards-card.tsx | 7 +- .../wallet/components/recharge-form-card.tsx | 63 +++--- .../components/subscription-plans-card.tsx | 69 ++++--- .../wallet/components/wallet-stats-card.tsx | 66 ++++--- web/default/src/i18n/locales/en.json | 1 + web/default/src/i18n/locales/fr.json | 1 + web/default/src/i18n/locales/ja.json | 1 + web/default/src/i18n/locales/ru.json | 1 + web/default/src/i18n/locales/vi.json | 1 + web/default/src/i18n/locales/zh-TW.json | 3 +- web/default/src/i18n/locales/zh.json | 3 +- web/default/src/lib/avatar.ts | 2 +- web/default/src/lib/theme-customization.ts | 2 +- web/default/src/styles/theme.css | 82 ++++---- 75 files changed, 1553 insertions(+), 655 deletions(-) create mode 100644 web/default/src/components/ui/icon-badge.tsx create mode 100644 web/default/src/features/keys/components/api-key-timestamp-cell.tsx create mode 100644 web/default/src/features/pricing/components/model-billing-mode-badge.tsx create mode 100644 web/default/src/features/redemption-codes/components/redemptions-mobile-list.tsx diff --git a/web/default/src/components/config-drawer.tsx b/web/default/src/components/config-drawer.tsx index 5eda5152e4..6e0d18ccb9 100644 --- a/web/default/src/components/config-drawer.tsx +++ b/web/default/src/components/config-drawer.tsx @@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com import { Radio as RadioPrimitive } from '@base-ui/react/radio' import { RadioGroup as Radio } from '@base-ui/react/radio-group' import { CircleCheck, Palette, RotateCcw } from 'lucide-react' -import { type SVGProps } from 'react' +import type { SVGProps } from 'react' import { useTranslation } from 'react-i18next' import { IconDir } from '@/assets/custom/icon-dir' @@ -277,16 +277,12 @@ function PresetConfig() { ) } diff --git a/web/default/src/components/drawer-layout.ts b/web/default/src/components/drawer-layout.ts index 4dd0251181..12ef7ee64f 100644 --- a/web/default/src/components/drawer-layout.ts +++ b/web/default/src/components/drawer-layout.ts @@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import { createElement, type ReactNode } from 'react' +import { IconBadge, type IconBadgeTone } from '@/components/ui/icon-badge' import { cn } from '@/lib/utils' export const sideDrawerContentClassName = (className?: string) => @@ -71,6 +72,7 @@ export function SideDrawerSectionHeader(props: { title: ReactNode description?: ReactNode icon?: ReactNode + iconTone?: IconBadgeTone className?: string }) { return createElement( @@ -78,11 +80,8 @@ export function SideDrawerSectionHeader(props: { { className: cn('flex items-start gap-3', props.className) }, props.icon ? createElement( - 'span', - { - className: - 'bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-md', - }, + IconBadge, + { tone: props.iconTone, size: 'md' }, props.icon ) : null, diff --git a/web/default/src/components/ui/icon-badge.tsx b/web/default/src/components/ui/icon-badge.tsx new file mode 100644 index 0000000000..494889b499 --- /dev/null +++ b/web/default/src/components/ui/icon-badge.tsx @@ -0,0 +1,85 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { cva, type VariantProps } from 'class-variance-authority' +import type { ReactNode } from 'react' + +import { cn } from '@/lib/utils' + +const iconBadgeVariants = cva( + 'flex shrink-0 items-center justify-center [&>svg]:shrink-0', + { + variants: { + tone: { + neutral: 'bg-muted text-muted-foreground', + primary: 'bg-primary/10 text-primary', + success: 'bg-success/10 text-success', + warning: 'bg-warning/10 text-warning', + info: 'bg-info/10 text-info', + destructive: 'bg-destructive/10 text-destructive', + 'chart-1': 'bg-chart-1/10 text-chart-1', + 'chart-2': 'bg-chart-2/10 text-chart-2', + 'chart-3': 'bg-chart-3/10 text-chart-3', + 'chart-4': 'bg-chart-4/10 text-chart-4', + 'chart-5': 'bg-chart-5/10 text-chart-5', + }, + size: { + xs: 'size-5 rounded-md [&>svg]:size-3', + sm: 'size-7 rounded-md [&>svg]:size-3.5', + md: 'size-8 rounded-lg [&>svg]:size-4', + title: 'size-8 rounded-lg sm:size-9 [&>svg]:size-4', + lg: 'size-10 rounded-xl [&>svg]:size-5', + stat: 'size-5 rounded-md sm:size-7 [&>svg]:size-3 sm:[&>svg]:size-3.5', + }, + }, + defaultVariants: { + tone: 'neutral', + size: 'md', + }, + } +) + +export type IconBadgeTone = NonNullable< + VariantProps['tone'] +> + +export type IconBadgeSize = NonNullable< + VariantProps['size'] +> + +interface IconBadgeProps { + children?: ReactNode + tone?: IconBadgeTone + size?: IconBadgeSize + className?: string + decorative?: boolean +} + +export function IconBadge(props: IconBadgeProps) { + return ( + + {props.children} + + ) +} diff --git a/web/default/src/components/ui/sidebar.tsx b/web/default/src/components/ui/sidebar.tsx index 30175e8750..c1ab863bfb 100644 --- a/web/default/src/components/ui/sidebar.tsx +++ b/web/default/src/components/ui/sidebar.tsx @@ -477,7 +477,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
    ) diff --git a/web/default/src/components/ui/table.tsx b/web/default/src/components/ui/table.tsx index 2163c119c8..ababb9a5f3 100644 --- a/web/default/src/components/ui/table.tsx +++ b/web/default/src/components/ui/table.tsx @@ -54,7 +54,7 @@ function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) { return ( tr]:h-15 [&_tr:last-child]:border-0', className)} {...props} /> ) diff --git a/web/default/src/components/ui/titled-card.tsx b/web/default/src/components/ui/titled-card.tsx index 131ffb05f5..0c75691fbe 100644 --- a/web/default/src/components/ui/titled-card.tsx +++ b/web/default/src/components/ui/titled-card.tsx @@ -27,6 +27,7 @@ import { CardHeader, CardTitle, } from './card' +import { IconBadge, type IconBadgeTone } from './icon-badge' type TitledCardProps = { title: ReactNode @@ -39,6 +40,7 @@ type TitledCardProps = { headerClassName?: string contentClassName?: string iconClassName?: string + iconTone?: IconBadgeTone titleClassName?: string descriptionClassName?: string } @@ -54,6 +56,7 @@ export function TitledCard({ headerClassName, contentClassName, iconClassName, + iconTone, titleClassName, descriptionClassName, }: TitledCardProps) { @@ -68,14 +71,9 @@ export function TitledCard({
    {icon != null && ( -
    + {icon} -
    + )}
    - - + } >
    {/* Current Balance Display */}
    - + + + {t('Current Balance')}
    diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index e3b1aa7b34..b3aa067298 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -78,6 +78,7 @@ import { FormLabel, FormMessage, } from '@/components/ui/form' +import { IconBadge, type IconBadgeTone } from '@/components/ui/icon-badge' import { Input } from '@/components/ui/input' import { Select, @@ -365,25 +366,37 @@ function formatUnixTime(timestamp: unknown): string { return new Date(seconds * 1000).toLocaleString() } -function CardHeading({ title, icon }: { title: string; icon?: ReactNode }) { +function CardHeading(props: { + title: string + icon?: ReactNode + iconTone?: IconBadgeTone +}) { return (
    - {icon && ( - - {icon} - + {props.icon && ( + + {props.icon} + )} -

    {title}

    +

    {props.title}

    ) } -function SubHeading({ title, icon }: { title: string; icon?: ReactNode }) { +function SubHeading(props: { + title: string + icon?: ReactNode + iconTone?: IconBadgeTone +}) { return (
    - {icon && {icon}} + {props.icon && ( + + {props.icon} + + )}

    - {title} + {props.title}

    ) @@ -1823,9 +1836,9 @@ export function ChannelMutateDrawer({
    - + - + {isEditing ? t('Edit Channel') : t('Create Channel')} @@ -3583,6 +3596,7 @@ export function ChannelMutateDrawer({ } + iconTone='info' />
    } + iconTone='info' />
    } + iconTone='chart-3' />
    } + iconTone='chart-4' /> } + iconTone='chart-3' /> {sensitiveLocked && ( @@ -4231,6 +4249,7 @@ export function ChannelMutateDrawer({ } + iconTone='chart-4' />
    } + iconTone='info' />