feat: Implement useThemeLabelColor hook and update components to utilize it for label color management
This commit is contained in:
@@ -65,4 +65,30 @@ describe('ModelAnalysisPanel token summaries', () => {
|
|||||||
|
|
||||||
root?.unmount();
|
root?.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders with fallback label color when browser theme APIs are unavailable', async () => {
|
||||||
|
globalThis.document = {
|
||||||
|
documentElement: {},
|
||||||
|
} as unknown as Document;
|
||||||
|
delete (globalThis as typeof globalThis & { getComputedStyle?: typeof getComputedStyle }).getComputedStyle;
|
||||||
|
delete (globalThis as typeof globalThis & { MutationObserver?: typeof MutationObserver }).MutationObserver;
|
||||||
|
|
||||||
|
let root: ReturnType<typeof create> | null = null;
|
||||||
|
|
||||||
|
await expect(act(async () => {
|
||||||
|
root = create(
|
||||||
|
<ModelAnalysisPanel
|
||||||
|
data={{
|
||||||
|
totals: {
|
||||||
|
spend: 0.123456,
|
||||||
|
calls: 10,
|
||||||
|
tokens: 611_540_335,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
})).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
root?.unmount();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { VChart } from '@visactor/react-vchart';
|
import { VChart } from '@visactor/react-vchart';
|
||||||
import { InlineBrandIcon } from './BrandIcon.js';
|
import { InlineBrandIcon } from './BrandIcon.js';
|
||||||
import { formatCompactTokenMetric } from '../numberFormat.js';
|
import { formatCompactTokenMetric } from '../numberFormat.js';
|
||||||
|
import { useThemeLabelColor } from './useThemeLabelColor.js';
|
||||||
|
|
||||||
type TabKey = 'spend' | 'trend' | 'calls' | 'rank';
|
type TabKey = 'spend' | 'trend' | 'calls' | 'rank';
|
||||||
|
|
||||||
@@ -56,19 +57,7 @@ function EmptyBlock() {
|
|||||||
|
|
||||||
export default function ModelAnalysisPanel({ data }: ModelAnalysisPanelProps) {
|
export default function ModelAnalysisPanel({ data }: ModelAnalysisPanelProps) {
|
||||||
const [activeTab, setActiveTab] = useState<TabKey>('spend');
|
const [activeTab, setActiveTab] = useState<TabKey>('spend');
|
||||||
|
const labelColor = useThemeLabelColor();
|
||||||
/* VChart uses Canvas — CSS variables don't work. Read the computed value. */
|
|
||||||
const [labelColor, setLabelColor] = useState('#9ca3af');
|
|
||||||
useEffect(() => {
|
|
||||||
const read = () => {
|
|
||||||
const c = getComputedStyle(document.documentElement).getPropertyValue('--color-text-secondary').trim();
|
|
||||||
if (c) setLabelColor(c);
|
|
||||||
};
|
|
||||||
read();
|
|
||||||
const obs = new MutationObserver(read);
|
|
||||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
||||||
return () => obs.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const totals = {
|
const totals = {
|
||||||
spend: toSafeNumber(data?.totals?.spend),
|
spend: toSafeNumber(data?.totals?.spend),
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { act, create } from 'react-test-renderer';
|
||||||
|
import SiteDistributionChart from './SiteDistributionChart.js';
|
||||||
|
|
||||||
|
vi.mock('@visactor/react-vchart', () => ({
|
||||||
|
VChart: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('SiteDistributionChart', () => {
|
||||||
|
const originalDocument = globalThis.document;
|
||||||
|
const originalGetComputedStyle = globalThis.getComputedStyle;
|
||||||
|
const originalMutationObserver = globalThis.MutationObserver;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
globalThis.document = {
|
||||||
|
documentElement: {
|
||||||
|
getAttribute: vi.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as Document;
|
||||||
|
delete (globalThis as typeof globalThis & { getComputedStyle?: typeof getComputedStyle }).getComputedStyle;
|
||||||
|
delete (globalThis as typeof globalThis & { MutationObserver?: typeof MutationObserver }).MutationObserver;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.document = originalDocument;
|
||||||
|
globalThis.getComputedStyle = originalGetComputedStyle;
|
||||||
|
globalThis.MutationObserver = originalMutationObserver;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with fallback label color when browser theme APIs are unavailable', async () => {
|
||||||
|
let renderer: ReturnType<typeof create> | null = null;
|
||||||
|
|
||||||
|
await expect(act(async () => {
|
||||||
|
renderer = create(
|
||||||
|
<SiteDistributionChart
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
siteName: 'Demo Site',
|
||||||
|
platform: 'demo',
|
||||||
|
totalBalance: 12.34,
|
||||||
|
totalSpend: 1.23,
|
||||||
|
accountCount: 2,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
})).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
renderer?.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { VChart } from '@visactor/react-vchart';
|
import { VChart } from '@visactor/react-vchart';
|
||||||
|
import { useThemeLabelColor } from '../useThemeLabelColor.js';
|
||||||
|
|
||||||
interface SiteDistributionData {
|
interface SiteDistributionData {
|
||||||
siteName: string;
|
siteName: string;
|
||||||
@@ -86,6 +87,7 @@ function EmptyState() {
|
|||||||
|
|
||||||
export default function SiteDistributionChart({ data, loading }: SiteDistributionChartProps) {
|
export default function SiteDistributionChart({ data, loading }: SiteDistributionChartProps) {
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('balance');
|
const [viewMode, setViewMode] = useState<ViewMode>('balance');
|
||||||
|
const labelColor = useThemeLabelColor();
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!data || data.length === 0) return [];
|
if (!data || data.length === 0) return [];
|
||||||
@@ -99,19 +101,6 @@ export default function SiteDistributionChart({ data, loading }: SiteDistributio
|
|||||||
|
|
||||||
const hasData = chartData.length > 0 && chartData.some((d) => d.value > 0);
|
const hasData = chartData.length > 0 && chartData.some((d) => d.value > 0);
|
||||||
|
|
||||||
/* VChart uses Canvas — CSS variables don't work for label colors. Read the computed value. */
|
|
||||||
const [labelColor, setLabelColor] = useState('#9ca3af');
|
|
||||||
React.useEffect(() => {
|
|
||||||
const read = () => {
|
|
||||||
const c = getComputedStyle(document.documentElement).getPropertyValue('--color-text-secondary').trim();
|
|
||||||
if (c) setLabelColor(c);
|
|
||||||
};
|
|
||||||
read();
|
|
||||||
const obs = new MutationObserver(read);
|
|
||||||
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
||||||
return () => obs.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const PIE_COLORS = ['#4f46e5', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
|
const PIE_COLORS = ['#4f46e5', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
|
||||||
|
|
||||||
const spec = useMemo(() => {
|
const spec = useMemo(() => {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
function readThemeLabelColor(): string | null {
|
||||||
|
if (typeof document === 'undefined') return null;
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (!root || typeof globalThis.getComputedStyle !== 'function') return null;
|
||||||
|
|
||||||
|
const color = globalThis.getComputedStyle(root).getPropertyValue('--color-text-secondary').trim();
|
||||||
|
return color || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useThemeLabelColor(fallback = '#9ca3af'): string {
|
||||||
|
const [labelColor, setLabelColor] = useState(fallback);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = typeof document !== 'undefined' ? document.documentElement : null;
|
||||||
|
const read = () => {
|
||||||
|
const color = readThemeLabelColor();
|
||||||
|
if (color) setLabelColor(color);
|
||||||
|
};
|
||||||
|
|
||||||
|
read();
|
||||||
|
|
||||||
|
if (!root || typeof globalThis.MutationObserver !== 'function') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new globalThis.MutationObserver(read);
|
||||||
|
observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] });
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return labelColor;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user