import type { SxProps, Theme } from '@mui/material' import type { CategorySize } from './types' // Size-keyed bar styles — scoped strictly to the bar primitive // (track + fill height & corner radius). `small` preserves the // historical look (4px-tall pill); `medium` renders a 12px-tall track // with a 2px corner radius. Every other token (gap, padding, hover, // transitions, label/value typography) is size-invariant. const bar: Record> = { small: { position: 'relative', height: 4, bgcolor: 'action.hover', borderRadius: 1, overflow: 'hidden', transition: 'background-color 150ms ease', }, medium: { position: 'relative', height: 12, bgcolor: 'action.hover', borderRadius: 0.25, overflow: 'hidden', transition: 'background-color 150ms ease', }, } const barFill: Record> = { small: { position: 'absolute', inset: '0 auto 0 0', borderRadius: 1, transition: 'width 300ms ease-in-out, background-color 150ms ease, filter 150ms ease', }, medium: { position: 'absolute', inset: '0 auto 0 0', borderRadius: 0.25, transition: 'width 300ms ease-in-out, background-color 150ms ease, filter 150ms ease', }, } // Square-cornered segment for the stacked bar. Adjacent segments meet // cleanly; the parent `bar[size]` track clips the outer ends via its // own borderRadius + overflow:hidden, so the stacked bar still has the // same rounded silhouette as a single `CategoryBar`. const stackedSegment: Record> = { small: { position: 'absolute', top: 0, bottom: 0, transition: 'left 300ms ease-in-out, width 300ms ease-in-out, background-color 150ms ease, filter 150ms ease', }, medium: { position: 'absolute', top: 0, bottom: 0, transition: 'left 300ms ease-in-out, width 300ms ease-in-out, background-color 150ms ease, filter 150ms ease', }, } export const styles = { // ── Top-level container ──────────────────────────────────────────── root: { display: 'flex', flexDirection: 'column', position: 'relative', width: '100%', }, // Flat list of category rows. Holds both single + multi rows. list: { display: 'flex', flexDirection: 'column', gap: 1, py: 0.5, }, // Layered on `list` only in scroll mode (`maxItems === 0`): right padding // so the bars + right-aligned values clear the scrollbar. Right side only // — the left edge stays at 0 so content stays aligned with the capped // view. `overflowY` + the dynamic `maxHeight` remain on the inline style. listScroll: { pr: 1, }, // ── Single-series row ───────────────────────────────────────────── // v1 layout: label + value share a top flex row (justify-between); the // bar fills the row width below. Mirrors `rowMulti` for visual // consistency. rowSingle: { display: 'flex', flexDirection: 'column', gap: 0.5, // py: 0.5, // px: 0.5, cursor: 'pointer', userSelect: 'none', transition: 'background-color 80ms ease', // Hovering anywhere in the row also brightens the bar fill, matching // what the bar got from `bar:hover` before. `data-bar-fill` is set // on `barFill` by ; descendant selector reaches it // through `bar` in single rows and through `multiBarRow > bar` in // multi rows. '&:hover [data-bar-fill="true"]': { filter: 'brightness(1.2)', }, '&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', outlineOffset: -2, }, }, // Top row of `rowSingle`: label on the left, value on the right. rowHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 1, }, // ── Multi-series row ────────────────────────────────────────────── // Flex column: label fills the top row; bar+value pairs stack below. // Bars get full track width — better legibility for N>=2 series than a // shared grid would give. rowMulti: { display: 'flex', flexDirection: 'column', gap: 0.5, // py: 0.5, // px: 0.5, cursor: 'pointer', userSelect: 'none', transition: 'background-color 80ms ease', // Row-hover brightens every bar fill inside the multi-row. '&:hover [data-bar-fill="true"]': { filter: 'brightness(1.2)', }, '&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', outlineOffset: -2, }, }, // ── Row labels / values ─────────────────────────────────────────── // Matches v1: name uses medium-weight body2 in primary text color, // value uses regular body2 in secondary text color. Same font size, // visual hierarchy via weight + color. name: { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', color: 'text.primary', fontWeight: 'medium', }, multiName: { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', color: 'text.primary', fontWeight: 'medium', }, value: { color: 'text.secondary', fontVariantNumeric: 'tabular-nums', // Right-align + minWidth keeps `rowMulti`'s per-bar value column // tidy (where the bar grows on the left and the number sits at a // fixed right edge). `rowSingle` uses `rowHeader`'s flex // justify-between to push the value to the trailing edge, so the // minWidth here is also harmless in that context. minWidth: 48, textAlign: 'right', }, // ── Bar (track + fill) ──────────────────────────────────────────── // Size-keyed maps live above the `styles` object; `` picks // the variant via its `size` prop. Hover brightening still fires from // the parent row (`&:hover [data-bar-fill]`), so it works regardless // of which size variant the bar uses. bar, barFill, stackedSegment, // ── Multi-series internals ──────────────────────────────────────── // One [bar | value] flex row per series, inside `rowMulti`'s column. multiBarRow: { display: 'flex', alignItems: 'center', gap: 1, }, multiBarContainer: { flex: 1, }, // ── Stacked-mode row ────────────────────────────────────────────── // Mirror `rowMulti`'s outer shape (click target + hover brightening + // focus ring) but stack three children vertically: header line, bar, // breakdown line. rowStacked: { display: 'flex', flexDirection: 'column', gap: 0.5, cursor: 'pointer', userSelect: 'none', transition: 'background-color 80ms ease', '&:hover [data-bar-fill="true"]': { filter: 'brightness(1.2)', }, '&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', outlineOffset: -2, }, }, stackedHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 1, }, // Right-aligned total in the header. Tabular-nums keeps totals lined // up vertically across rows; minWidth reuses the per-series-value // column width so the right edge sits at the same spot as `rowMulti`'s // value column when both modes co-exist in the same widget card. stackedTotal: { color: 'text.primary', fontWeight: 'medium', fontVariantNumeric: 'tabular-nums', minWidth: 48, textAlign: 'right', }, // Flex row of "{seriesName}: {value}" pairs. Wraps gracefully when // series names are long or there are many series. stackedBreakdown: { display: 'flex', flexWrap: 'wrap', columnGap: 2, rowGap: 0.25, }, stackedBreakdownItem: { color: 'text.secondary', fontVariantNumeric: 'tabular-nums', }, // ── Legend (sticky bottom) ──────────────────────────────────────── legend: { display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap', pt: 2, mt: 2, position: 'sticky', bottom: 0, bgcolor: 'background.paper', borderTop: (theme: Theme) => `1px solid ${theme.palette.divider}`, }, legendItem: { display: 'flex', alignItems: 'center', gap: 1, }, legendDot: { width: 8, height: 8, borderRadius: '50%', }, legendLabel: { color: 'text.secondary', textTransform: 'uppercase', fontWeight: 'medium', }, // ── "Other" overflow row ────────────────────────────────────────── // Left-aligned `Others ` — the label sits next to a muted count // (no parentheses). Only the label is the click target when interactive; // the count is always static text beside it. otherRow: { display: 'flex', alignItems: 'center', gap: 1, py: 0.5, px: 0.5, }, // Interactive affordances layered onto the "Others" label only (cursor, // hover tint, focus ring). A negative LEFT margin + matching padding keep // the hover pill breathing without shifting the label's text edge (it // stays aligned with the category rows above). No right margin, so the // row `gap` is preserved between the pill and the count. otherLabelButton: { cursor: 'pointer', userSelect: 'none', borderRadius: 1, px: 0.5, ml: -0.5, transition: 'background-color 80ms ease', '&:hover': { bgcolor: 'action.hover', }, '&:focus-visible': { outline: '2px solid', outlineColor: 'primary.main', outlineOffset: 2, }, }, otherLabel: { color: 'text.primary', fontWeight: 'medium', }, otherCount: { color: 'text.secondary', fontVariantNumeric: 'tabular-nums', }, // `bar` / `barFill` are size-keyed (`Record`); // every other entry is a single `SxProps`. The mixed shape means // we can't apply the file-wide `satisfies Record>` // constraint that the other widget style files use — consumers of // individual tokens still get full type-checking at the call site via // the typed `Box`/`Typography` `sx` prop. }