diff --git a/apps/frontend/src/renderer/__tests__/ui-charts.test.ts b/apps/frontend/src/renderer/__tests__/ui-charts.test.ts index 723c7fe8a..8183d0d71 100644 --- a/apps/frontend/src/renderer/__tests__/ui-charts.test.ts +++ b/apps/frontend/src/renderer/__tests__/ui-charts.test.ts @@ -4,7 +4,7 @@ */ import { describe, expect, it } from 'vitest'; -import { pointsToArea, seriesToPoints } from '@auto-code/ui'; +import { barPercent, pointsToArea, seriesToPoints } from '@auto-code/ui'; describe('seriesToPoints', () => { it('spreads points evenly across the width and inverts Y', () => { @@ -50,3 +50,19 @@ describe('pointsToArea', () => { expect(pointsToArea('', 100, 40)).toBe(''); }); }); + +describe('barPercent', () => { + it('returns the value share of the scale, clamped to 0-100', () => { + expect(barPercent(26, 26)).toBe(100); + expect(barPercent(13, 26)).toBe(50); + expect(barPercent(1, 26)).toBeCloseTo(3.85, 2); + expect(barPercent(40, 26)).toBe(100); + }); + + it('yields 0 for non-positive value or scale', () => { + expect(barPercent(0, 26)).toBe(0); + expect(barPercent(-5, 26)).toBe(0); + expect(barPercent(5, 0)).toBe(0); + expect(barPercent(Number.NaN, 26)).toBe(0); + }); +}); diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index 3e36f74a4..5287c681c 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -16,7 +16,9 @@ export type { } from './primitives/LineChart'; export { StatTile } from './primitives/StatTile'; export type { StatTileProps, StatTileTone } from './primitives/StatTile'; -export { seriesToPoints, pointsToArea, CHART_TONE_VARS } from './primitives/charts'; +export { BarList } from './primitives/BarList'; +export type { BarListProps, BarListItem } from './primitives/BarList'; +export { seriesToPoints, pointsToArea, barPercent, CHART_TONE_VARS } from './primitives/charts'; export type { ChartTone } from './primitives/charts'; export { ThemeProvider, useTheme } from './theme/ThemeProvider'; export type { Theme, ResolvedTheme, ThemeProviderProps } from './theme/ThemeProvider'; diff --git a/libs/ui/src/primitives/BarList.css b/libs/ui/src/primitives/BarList.css new file mode 100644 index 000000000..ef1b72622 --- /dev/null +++ b/libs/ui/src/primitives/BarList.css @@ -0,0 +1,40 @@ +.ac-barlist { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +} +.ac-barlist__row { + display: grid; + grid-template-columns: minmax(80px, auto) minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + font-size: 12px; +} +.ac-barlist__label { + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.ac-barlist__track { + height: 8px; + border-radius: 999px; + background: var(--rail); + overflow: hidden; +} +.ac-barlist__bar { + display: block; + height: 100%; + border-radius: 999px; + min-width: 2px; +} +.ac-barlist__value { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 11.5px; + color: var(--ink); + font-weight: 720; + text-align: right; +} diff --git a/libs/ui/src/primitives/BarList.stories.tsx b/libs/ui/src/primitives/BarList.stories.tsx new file mode 100644 index 000000000..d15656a07 --- /dev/null +++ b/libs/ui/src/primitives/BarList.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { BarList } from './BarList'; + +const meta = { + title: 'Primitives/BarList', + component: BarList, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + args: { + ariaLabel: 'PR size distribution', + items: [ + { label: 'XS <50', value: 26, tone: 'good' }, + { label: 'S 50–200', value: 21, tone: 'good' }, + { label: 'M 200–500', value: 10, tone: 'info' }, + { label: 'L 500–1k', value: 4, tone: 'warn' }, + { label: 'XL >1k', value: 1, tone: 'bad' }, + ], + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Distribution: Story = {}; + +export const WithValueLabels: Story = { + args: { + ariaLabel: 'Tokens by model', + items: [ + { label: 'Sonnet 4.6', value: 1_240_000, valueLabel: '1.2M', tone: 'info' }, + { label: 'Haiku 4.5', value: 680_000, valueLabel: '680k', tone: 'good' }, + { label: 'Opus 4.8', value: 210_000, valueLabel: '210k', tone: 'warn' }, + ], + }, +}; + +export const SingleRow: Story = { + args: { ariaLabel: 'One category', items: [{ label: 'Only', value: 7 }] }, +}; diff --git a/libs/ui/src/primitives/BarList.tsx b/libs/ui/src/primitives/BarList.tsx new file mode 100644 index 000000000..08bca9cdf --- /dev/null +++ b/libs/ui/src/primitives/BarList.tsx @@ -0,0 +1,66 @@ +import { CHART_TONE_VARS, barPercent, type ChartTone } from './charts'; +import './BarList.css'; + +export interface BarListItem { + label: string; + value: number; + /** Pre-formatted value shown at the row end; defaults to the raw value. */ + valueLabel?: string; + tone?: ChartTone; +} + +export interface BarListProps { + items: readonly BarListItem[]; + /** Bar scale maximum; defaults to the largest item value (min 1). */ + max?: number; + /** Accessible name for the whole list (rendered on the