Chart builder: equal shelf heights, pill floor, flat field rows, scroll affordance

This commit is contained in:
2026-06-12 23:13:26 +03:00
parent 7d4906ec38
commit b8f0b97f22
2 changed files with 87 additions and 108 deletions
+24 -24
View File
@@ -71,6 +71,8 @@ import {
useChartBuilderStore,
} from '../stores/ChartBuilderStore';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
import { Button } from './Button';
import { IconButton } from './IconButton';
import { SelectControl } from './SelectControl';
import { Icon } from './Icon';
import styles from './ChartBuilderModal.module.css';
@@ -579,9 +581,11 @@ function FieldShelf() {
<div className={`${styles.shelfScroll} ${activeChannel ? styles.shelfArmed : ''}`}>
{split ? (
<>
<span className={styles.shelfGroupHead}>Dimensions</span>
{/* The visible count says "there's more below the fold" — the grouped
shelf only appears for wide datasets, where the list scrolls. */}
<span className={styles.shelfGroupHead}>Dimensions · {dimensions.length}</span>
<div className={styles.shelfList}>{dimensions.map(fieldButton)}</div>
<span className={styles.shelfGroupHead}>Measures</span>
<span className={styles.shelfGroupHead}>Measures · {measures.length + 1}</span>
<div className={styles.shelfList}>
{measures.map(fieldButton)}
{countButton}
@@ -618,6 +622,8 @@ function MarksCard() {
/** A reserved, non-interactive shelf slot for faceting (small multiples) — Phase 4.
* Shown so the layout telegraphs where row/column faceting will live. */
// TODO: the "later" tag is internal roadmap language shown to users — hide the
// affordance until faceting ships, or reword for users (docs/ux-second-pass.md).
function FacetSlot({ kind }: { kind: 'column' | 'row' }) {
return (
<div className={styles.facetSlot} title="Faceting → small multiples (coming later)">
@@ -776,14 +782,9 @@ function FilterRow({ filter, columns }: { filter: BuilderFilter; columns: Builde
triggerContent={filter.field ? undefined : 'Choose a field…'}
/>
)}
<button
type="button"
className={styles.removeRow}
aria-label="Remove filter"
onClick={() => removeFilter(filter.id)}
>
<IconButton size="sm" label="Remove filter" onClick={() => removeFilter(filter.id)}>
<Icon name="close" />
</button>
</IconButton>
</div>
{!expressionMode && (
@@ -876,14 +877,13 @@ function CalculateRow({ calc, columns }: { calc: BuilderCalculate; columns: Buil
aria-describedby={exprMsgId}
onChange={(e) => updateCalculate(calc.id, { expr: e.target.value })}
/>
<button
type="button"
className={styles.removeRow}
aria-label="Remove calculated field"
<IconButton
size="sm"
label="Remove calculated field"
onClick={() => removeCalculate(calc.id)}
>
<Icon name="close" />
</button>
</IconButton>
</div>
<ExprFeedback expr={calc.expr} columns={columns} messageId={exprMsgId} />
</div>
@@ -1303,16 +1303,16 @@ function NoDatasets() {
The Chart Builder turns a saved dataset into a chart pick columns, watch the chart take
shape, and save it as a snippet. Add a dataset to start building.
</p>
<button
type="button"
className={`${styles.action} ${styles.primary}`}
<Button
variant="primary"
size="lg"
onClick={() => {
openModal('datasets');
useDatasetStore.getState().startCreate();
}}
>
Add a dataset
</button>
</Button>
</div>
);
}
@@ -1470,12 +1470,12 @@ export function ChartBuilderModal() {
</p>
)}
<div className={styles.actions}>
<button type="button" className={styles.action} onClick={() => void closeModal()}>
<Button size="lg" onClick={() => void closeModal()}>
Cancel
</button>
<button
type="button"
className={`${styles.action} ${styles.primary}`}
</Button>
<Button
variant="primary"
size="lg"
disabled={!valid}
aria-describedby={!valid ? 'cb-create-hint' : undefined}
// The create is the user's confirmation — close with no discard prompt.
@@ -1484,7 +1484,7 @@ export function ChartBuilderModal() {
}}
>
Create Snippet
</button>
</Button>
</div>
</div>