diff --git a/apps/ui/src/components/AnalysisResults.tsx b/apps/ui/src/components/AnalysisResults.tsx index c7990ecb..a34e6e9c 100644 --- a/apps/ui/src/components/AnalysisResults.tsx +++ b/apps/ui/src/components/AnalysisResults.tsx @@ -40,11 +40,7 @@ import { TranscriptionPianorollBlock } from './TranscriptionPianorollBlock'; import { Mt3TranscriptionPanel } from './Mt3TranscriptionPanel'; import { StemListeningNotesPanel } from './StemListeningNotesPanel'; import { hasStemListeningNotesContent } from '../services/sessionMusician'; -import { - StatusBadge, - TokenBadgeList, -} from './MeasurementPrimitives'; -import { Button, DeviceRack, MetricBar, MetricTile, Pill, SectionHeader } from './ui'; +import { Button, DeviceRack, MetricBar, MetricTile, Pill, SectionHeader, TokenBadgeList } from './ui'; import { PhaseSourceBadge } from './PhaseSourceBadge'; import { StickyNav, type StickyNavSection } from './StickyNav'; import { CitationBlock, CitationHeadline } from './CitationBlock'; @@ -1048,7 +1044,7 @@ export function AnalysisResults({ icon={} label="METER" value={phase1.timeSignature} - footer={} + footer={{meterStatusLabel(phase1)}} /> {/* CHARACTER — genre primary, characteristic pills secondary */} @@ -1066,7 +1062,7 @@ export function AnalysisResults({ items={[ { label: phase1.genreDetail.genreFamily, tone: 'accent' }, ...(phase1.genreDetail.secondaryGenre - ? [{ label: phase1.genreDetail.secondaryGenre, tone: 'muted' as const }] + ? [{ label: phase1.genreDetail.secondaryGenre, tone: 'neutral' as const }] : []), ]} /> @@ -1418,7 +1414,7 @@ export function AnalysisResults({ Instruments

({ label: item, tone: 'muted' as const }))} + items={styleProfile.instruments.map((item) => ({ label: item, tone: 'neutral' as const }))} /> )} @@ -1428,7 +1424,7 @@ export function AnalysisResults({ Production Techniques

({ label: item, tone: 'violet' as const }))} + items={styleProfile.productionTechniques.map((item) => ({ label: item, tone: 'neutral' as const }))} /> )} diff --git a/apps/ui/src/components/MixDoctorPanel.tsx b/apps/ui/src/components/MixDoctorPanel.tsx index e5d55fd3..c7c492db 100644 --- a/apps/ui/src/components/MixDoctorPanel.tsx +++ b/apps/ui/src/components/MixDoctorPanel.tsx @@ -1,10 +1,6 @@ import React from 'react'; import type { MixDoctorReport, MixDynamicsIssue, MixIssue } from '../services/mixDoctor'; -import { - DeltaBadge, - StatusBadge, -} from './MeasurementPrimitives'; -import { DataTable, MetricTile, Panel } from './ui'; +import { DataTable, DeltaBadge, MetricTile, Panel, Pill } from './ui'; import { getTextRoleClassName } from '../utils/displayText'; interface MixDoctorPanelProps { @@ -59,17 +55,16 @@ export function MixDoctorPanel({ report }: MixDoctorPanelProps) { accent="accent" label="Target Genre" value={report.genreName} - footer={} + footer={{report.genreId}} /> + + {`${report.overallScore}/100`} + } /> ( - + {item.label}

{item.message}

))} @@ -176,7 +171,7 @@ export function MixDoctorPanel({ report }: MixDoctorPanelProps) { label: 'Issue', render: (row) => (
- + {row.issue}
), }, diff --git a/apps/ui/src/components/ui/DeltaBadge.tsx b/apps/ui/src/components/ui/DeltaBadge.tsx new file mode 100644 index 00000000..39c17250 --- /dev/null +++ b/apps/ui/src/components/ui/DeltaBadge.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import { Pill } from './Pill'; +import type { Tone } from './variants'; + +export interface DeltaBadgeProps { + value: number | null | undefined; + unit?: string; + decimals?: number; + okThreshold: number; + warnThreshold: number; + invert?: boolean; + showSign?: boolean; + className?: string; +} + +function formatSignedNumber(value: number, decimals: number, showSign: boolean): string { + const abs = Math.abs(value).toFixed(decimals); + if (!showSign) return value.toFixed(decimals); + return `${value >= 0 ? '+' : '-'}${abs}`; +} + +/** + * Threshold-driven delta chip. Renders a {@link Pill} whose tone reflects how + * far `value` sits from zero relative to the ok/warn thresholds (or the inverse + * when `invert`). Non-finite values render an `n/a` neutral pill. Migrated from + * the retired MeasurementPrimitives layer onto the canonical token palette. + */ +export function DeltaBadge({ + value, + unit, + decimals = 1, + okThreshold, + warnThreshold, + invert = false, + showSign = true, + className, +}: DeltaBadgeProps) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return ( + + n/a + + ); + } + + const magnitude = Math.abs(value); + const tone: Tone = + magnitude <= okThreshold + ? invert + ? 'error' + : 'success' + : magnitude <= warnThreshold + ? 'warning' + : invert + ? 'success' + : 'error'; + const label = `${formatSignedNumber(value, decimals, showSign)}${unit ? ` ${unit}` : ''}`; + + return ( + + {label} + + ); +} diff --git a/apps/ui/src/components/ui/TokenBadgeList.tsx b/apps/ui/src/components/ui/TokenBadgeList.tsx new file mode 100644 index 00000000..5ad2fddb --- /dev/null +++ b/apps/ui/src/components/ui/TokenBadgeList.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import { cn } from './cn'; +import { Pill } from './Pill'; +import type { Tone } from './variants'; + +export interface TokenBadgeListProps { + items: Array<{ label: string; tone?: Tone }>; + className?: string; +} + +/** + * A wrapped row of {@link Pill} chips for tag/token lists (genres, instruments, + * production techniques, …). Migrated from the retired MeasurementPrimitives + * layer; tones are restricted to the token palette (off-palette tones collapse + * to `neutral` at the call site). + */ +export function TokenBadgeList({ items, className }: TokenBadgeListProps) { + return ( +
+ {items.map((item, index) => ( + + {item.label} + + ))} +
+ ); +} diff --git a/apps/ui/src/components/ui/index.ts b/apps/ui/src/components/ui/index.ts index 05553333..7f18a0f5 100644 --- a/apps/ui/src/components/ui/index.ts +++ b/apps/ui/src/components/ui/index.ts @@ -9,6 +9,7 @@ export { type DataTableColumn, type DataTableProps, } from './DataTable'; +export { DeltaBadge, type DeltaBadgeProps } from './DeltaBadge'; export { DeviceRack, deviceRackVariants, @@ -41,4 +42,5 @@ export { type SignalStageStatus, } from './SignalChain'; export { TimeReadout, type TimeReadoutProps } from './TimeReadout'; +export { TokenBadgeList, type TokenBadgeListProps } from './TokenBadgeList'; export { Tooltip, TooltipProvider, type TooltipProps } from './Tooltip'; diff --git a/apps/ui/tests/services/mixDoctorPanel.test.ts b/apps/ui/tests/services/mixDoctorPanel.test.ts index 296fa3ae..2c68669d 100644 --- a/apps/ui/tests/services/mixDoctorPanel.test.ts +++ b/apps/ui/tests/services/mixDoctorPanel.test.ts @@ -127,9 +127,9 @@ describe('MixDoctorPanel', () => { ); expect(html).toContain('86/100'); - expect(html).toContain('border-success/30 bg-success/10'); - expect(html).toContain('border-warning/30 bg-warning/10'); - expect(html).toContain('border-error/30 bg-error/10'); + expect(html).toContain('bg-success/20'); + expect(html).toContain('bg-warning/20'); + expect(html).toContain('bg-error/20'); expect(html).toContain('-1.8'); });