diff --git a/packages/app/src/components/ColorSwatchInput.module.scss b/packages/app/src/components/ColorSwatchInput.module.scss new file mode 100644 index 0000000000..90ab70122c --- /dev/null +++ b/packages/app/src/components/ColorSwatchInput.module.scss @@ -0,0 +1,39 @@ +.trigger { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 6px; + border-radius: 4px; + background-color: var(--color-bg-field); + border: 1px solid var(--color-border); + color: var(--color-text); + font-size: 11px; + line-height: 1; + cursor: pointer; + user-select: none; + transition: + background-color 100ms ease, + border-color 100ms ease; + + &:hover { + background-color: var(--color-bg-field-highlighted); + border-color: var(--color-border); + } + + &:focus-visible { + outline: 2px solid var(--color-outline-focus); + outline-offset: 1px; + } + + &[data-disabled] { + cursor: not-allowed; + opacity: 0.6; + } +} + +.swatchButton { + &[data-selected] { + background-color: var(--color-bg-field-highlighted); + box-shadow: inset 0 0 0 1.5px var(--color-text); + } +} diff --git a/packages/app/src/components/ColorSwatchInput.stories.tsx b/packages/app/src/components/ColorSwatchInput.stories.tsx index 50722f9560..2ec8f17bd0 100644 --- a/packages/app/src/components/ColorSwatchInput.stories.tsx +++ b/packages/app/src/components/ColorSwatchInput.stories.tsx @@ -1,16 +1,448 @@ import React from 'react'; +import { + Bar, + BarChart, + CartesianGrid, + Line, + LineChart, + ReferenceLine, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { + Box, + Card, + Divider, + Group, + Select, + Stack, + Text, + TextInput, +} from '@mantine/core'; import type { Meta } from '@storybook/nextjs'; +import { IconChartLine } from '@tabler/icons-react'; + +import { + CATEGORICAL_PALETTE_TOKENS, + CHART_PALETTE_TOKENS, + ChartPaletteToken, + getColorFromCSSToken, + SEMANTIC_PALETTE_TOKENS, +} from '@/utils'; import { ColorSwatchInput } from './ColorSwatchInput'; const meta = { + title: 'ColorSwatchInput', component: ColorSwatchInput, } satisfies Meta; export default meta; +// --------------------------------------------------------------------------- +// Tier 1: isolated stories (picker mechanics) +// --------------------------------------------------------------------------- + export const Default = () => { - const [color, setColor] = React.useState('#6610f2'); + const [value, setValue] = React.useState( + undefined, + ); + return ; +}; + +export const Selected = () => { + const [value, setValue] = React.useState( + 'chart-1', + ); + return ; +}; + +export const Disabled = () => ( + + + + +); + +export const WithCustomLabel = () => { + const [value, setValue] = React.useState( + undefined, + ); + return ( + + ); +}; + +/** + * One trigger per token, all pre-selected. Renders the full matrix so the + * design review can compare swatch sizes, hover states, and per-token + * contrast across themes without opening the popover thirteen times. + */ +export const AllTokensSelected = () => ( + + + Categorical + + + {CATEGORICAL_PALETTE_TOKENS.map(token => ( + + ))} + + + Semantic + + + {SEMANTIC_PALETTE_TOKENS.map(token => ( + + ))} + + +); + +/** + * Picker mounted alongside other form controls so reviewers can verify + * the focus order during keyboard nav (Tab into the picker, activate + * with Enter or Space, Tab between swatches, Esc closes). + */ +export const KeyboardNav = () => { + const [value, setValue] = React.useState(); + return ( + + + + + + ); +}; + +// --------------------------------------------------------------------------- +// Tier 2: in-context stories (product framing) +// +// Storybook-only mocks. No production imports of ChartSeriesEditor or +// ChartDisplaySettingsDrawer; those land in PRs 2b through 6b. The mocks +// approximate the layouts so the design review can rule on placement +// before any consumer PR opens. +// --------------------------------------------------------------------------- + +type SeriesMockData = { + name: string; + token: ChartPaletteToken | undefined; + lineStyle: 'solid' | 'dashed' | 'dotted'; +}; + +const DEFAULT_SERIES_MOCK: SeriesMockData[] = [ + { name: 'errors', token: 'chart-error', lineStyle: 'solid' }, + { name: 'warnings', token: 'chart-warning', lineStyle: 'dashed' }, + { name: 'successes', token: 'chart-success', lineStyle: 'solid' }, +]; - return setColor(value)} />; +const MOCK_TIME_DATA = Array.from({ length: 12 }).map((_, i) => ({ + t: `${i}:00`, + errors: Math.round(20 + 8 * Math.sin(i / 2)), + warnings: Math.round(40 + 12 * Math.cos(i / 3)), + successes: Math.round(120 + 25 * Math.sin(i / 4 + 1)), +})); + +const LINE_STYLE_DASHARRAY: Record = { + solid: '0', + dashed: '4 3', + dotted: '2 2', +}; + +function parseLineStyle(value: string | null): SeriesMockData['lineStyle'] { + if (value === 'dashed' || value === 'dotted') return value; + return 'solid'; +} + +function MockSeriesPreview({ + series, + variant, + height = 140, +}: { + series: SeriesMockData[]; + variant: 'line' | 'bar'; + height?: number; +}) { + const ChartCmp = variant === 'line' ? LineChart : BarChart; + return ( + + + + + + + {series.map(s => { + const color = s.token + ? getColorFromCSSToken(s.token) + : 'var(--color-text-muted)'; + return variant === 'line' ? ( + + ) : ( + + ); + })} + + + ); +} + +function SeriesRow({ + series, + onChange, +}: { + series: SeriesMockData; + onChange: (next: SeriesMockData) => void; +}) { + return ( + + + {series.name} + + + count(*) + + + onChange({ ...series, token })} + /> +