forked from boos/BoosAPI
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();
|
||||
});
|
||||
|
||||
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 { InlineBrandIcon } from './BrandIcon.js';
|
||||
import { formatCompactTokenMetric } from '../numberFormat.js';
|
||||
import { useThemeLabelColor } from './useThemeLabelColor.js';
|
||||
|
||||
type TabKey = 'spend' | 'trend' | 'calls' | 'rank';
|
||||
|
||||
@@ -56,19 +57,7 @@ function EmptyBlock() {
|
||||
|
||||
export default function ModelAnalysisPanel({ data }: ModelAnalysisPanelProps) {
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('spend');
|
||||
|
||||
/* 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 labelColor = useThemeLabelColor();
|
||||
|
||||
const totals = {
|
||||
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 { useThemeLabelColor } from '../useThemeLabelColor.js';
|
||||
|
||||
interface SiteDistributionData {
|
||||
siteName: string;
|
||||
@@ -86,6 +87,7 @@ function EmptyState() {
|
||||
|
||||
export default function SiteDistributionChart({ data, loading }: SiteDistributionChartProps) {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('balance');
|
||||
const labelColor = useThemeLabelColor();
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
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);
|
||||
|
||||
/* 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 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