From b8f0b97f22e21f6a176b4ebe044312076aa8fa68 Mon Sep 17 00:00:00 2001 From: Oleh Omelchenko Date: Fri, 12 Jun 2026 23:13:26 +0300 Subject: [PATCH] Chart builder: equal shelf heights, pill floor, flat field rows, scroll affordance --- .../components/ChartBuilderModal.module.css | 147 ++++++++---------- src/app/components/ChartBuilderModal.tsx | 48 +++--- 2 files changed, 87 insertions(+), 108 deletions(-) diff --git a/src/app/components/ChartBuilderModal.module.css b/src/app/components/ChartBuilderModal.module.css index ec8ef6d..d9bf3d5 100644 --- a/src/app/components/ChartBuilderModal.module.css +++ b/src/app/components/ChartBuilderModal.module.css @@ -131,7 +131,8 @@ .valueInput { flex: 1; min-width: 0; - padding: var(--space-1) var(--space-2); + height: var(--control-height); + padding: 0 var(--space-2); border: var(--border-width) solid var(--border-strong); border-radius: var(--radius); background: var(--bg); @@ -149,7 +150,8 @@ .exprInput { flex: 1; min-width: 0; - padding: var(--space-2) var(--space-3); + height: var(--control-height); + padding: 0 var(--space-3); border: var(--border-width) solid var(--border-strong); border-radius: var(--radius); background: var(--bg); @@ -161,7 +163,8 @@ .calcName { flex: 0 1 110px; min-width: 0; - padding: var(--space-2) var(--space-3); + height: var(--control-height); + padding: 0 var(--space-3); border: var(--border-width) solid var(--border-strong); border-radius: var(--radius); background: var(--bg); @@ -187,30 +190,7 @@ border-color: var(--support-error); } -/* Remove-row × — icon-only, quiet until hovered. */ -.removeRow { - flex: none; - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border: none; - background: transparent; - color: var(--text-placeholder); - cursor: pointer; - border-radius: var(--radius); -} - -.removeRow:hover { - background: var(--layer-01); - color: var(--text); -} - -.removeRow:focus-visible { - outline: 2px solid var(--focus); - outline-offset: 1px; -} +/* Remove-row × is the shared IconButton (sm). */ /* Expression escape-hatch toggle — a low-emphasis link, like the hint fixes. */ .modeToggle { @@ -450,7 +430,8 @@ .dimInput { width: 100px; - padding: var(--space-2) var(--space-3); + height: var(--control-height); + padding: 0 var(--space-3); border: var(--border-width) solid var(--border-strong); border-radius: var(--radius); background: var(--bg); @@ -540,6 +521,7 @@ text-align: right; } +/* Cancel / Create are shared Buttons (secondary / primary, lg — modal footer). */ .actions { display: flex; justify-content: flex-end; @@ -547,45 +529,6 @@ margin-top: auto; } -.action { - height: 36px; - padding: 0 var(--space-5); - border: var(--border-width) solid var(--border-strong); - border-radius: var(--radius); - background: transparent; - color: var(--text); - font: inherit; - font-weight: 500; - cursor: pointer; - transition: background var(--dur-fast) var(--ease); -} - -.action:hover { - background: var(--layer-01); -} - -.action:focus-visible { - outline: 2px solid var(--focus); - outline-offset: 2px; -} - -.primary { - background: var(--accent); - border-color: transparent; - color: var(--accent-contrast); - font-weight: 600; -} - -.primary:hover { - background: var(--accent-hover); -} - -.primary:disabled { - background: var(--layer-02, var(--layer-01)); - color: var(--text-placeholder); - cursor: not-allowed; -} - /* ── Right: live preview ─────────────────────────────────────────────── */ /* Right side = on-chart shelves stacked above the live preview (spec §06). */ @@ -694,6 +637,22 @@ gap: var(--space-2); max-height: 280px; overflow-y: auto; + /* Scroll shadows (background-attachment trick): a soft cue at the cut edge that + appears only while there is more list in that direction. The covers (first + two layers, attachment: local) ride with the content and hide the shadows at + the ends; rows are transparent so the shadows show through between them. */ + background: + linear-gradient(var(--bg) 30%, transparent), + linear-gradient(transparent, var(--bg) 70%) 0 100%, + linear-gradient(rgb(0 0 0 / 0.1), transparent), + linear-gradient(transparent, rgb(0 0 0 / 0.1)) 0 100%; + background-repeat: no-repeat; + background-size: + 100% 24px, + 100% 24px, + 100% 8px, + 100% 8px; + background-attachment: local, local, scroll, scroll; } /* An armed channel makes the shelf the live target — say so where the next click @@ -726,33 +685,39 @@ .shelfList { display: flex; flex-direction: column; - gap: 2px; } +/* Field rows are FLAT — hairline dividers and a hover fill, not per-row boxes: + 25 outlined rectangles read as noise, and flat rows are denser, so more of a + wide dataset is visible before the shelf scrolls (arch 09 §4 — boxes are for + inputs and drop targets, not list rows). */ .shelfField { display: flex; align-items: center; gap: var(--space-2); width: 100%; - padding: var(--space-2) var(--space-3); - border: var(--border-width) solid var(--border); - border-radius: var(--radius); - background: var(--bg); + padding: var(--space-2) var(--space-2); + border: none; + background: transparent; color: var(--text); font: inherit; font-size: 13px; text-align: left; cursor: pointer; + transition: background var(--dur-fast) var(--ease); +} + +.shelfField + .shelfField { + border-top: var(--border-width) solid var(--border); } .shelfField:hover { - border-color: var(--accent); background: var(--layer-01); } .shelfField:focus-visible { outline: 2px solid var(--focus); - outline-offset: 1px; + outline-offset: -1px; } .shelfFieldUsed { @@ -808,7 +773,12 @@ .pill { display: inline-flex; - align-items: center; + align-items: stretch; + /* One pill height, and a floor on width so "Count" and "Customer Name" read as + the same species; names stay content-sized above the floor (truncating to + equal widths would cost more than it buys). */ + height: 24px; + min-width: 96px; max-width: 100%; background: var(--accent); color: var(--accent-contrast); @@ -821,7 +791,9 @@ } .pillType { - padding: var(--space-2); + display: inline-flex; + align-items: center; + padding: 0 var(--space-2); border: none; background: color-mix(in srgb, #000 18%, var(--accent)); color: var(--accent-contrast); @@ -850,7 +822,9 @@ } .pillTag { - padding: var(--space-2); + display: inline-flex; + align-items: center; + padding: 0 var(--space-2); color: var(--text-secondary); font-size: 10px; text-transform: uppercase; @@ -861,7 +835,8 @@ /* Grow into any pill slack so the remove ✕ stays at the pill's right edge. */ flex: 1; min-width: 0; - padding: var(--space-2) var(--space-3); + align-self: center; + padding: 0 var(--space-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -872,7 +847,7 @@ display: inline-flex; align-items: center; justify-content: center; - padding: var(--space-2); + padding: 0 var(--space-2); border: none; background: transparent; color: inherit; @@ -922,7 +897,7 @@ display: flex; align-items: center; gap: var(--space-2); - min-height: 32px; + min-height: var(--control-height); padding: var(--space-1) var(--space-2); border: var(--border-width) dashed var(--border-strong); background: var(--layer-01); @@ -999,7 +974,10 @@ .shelfStrip { display: flex; align-items: stretch; - min-height: 42px; + /* One fixed minimum for BOTH strips — sized to the tallest occupant (a 32px + control + the slot padding), so Columns (which carries the Aggregate select) + and Rows stay the same height instead of each hugging its own content. */ + min-height: 48px; border: var(--border-width) solid var(--border); } @@ -1052,13 +1030,14 @@ .propInput { width: 200px; - padding: var(--space-1) var(--space-2); + height: var(--control-height); + padding: 0 var(--space-2); border: var(--border-width) solid var(--border-strong); border-radius: var(--radius); background: var(--bg); color: var(--text); font: inherit; - font-size: 12px; + font-size: 13px; } .propInput:disabled { @@ -1077,7 +1056,7 @@ align-items: center; gap: var(--space-2); min-width: 150px; - min-height: 32px; + min-height: var(--control-height); padding: var(--space-1) var(--space-3); border: var(--border-width) dashed var(--border); color: var(--text-placeholder); diff --git a/src/app/components/ChartBuilderModal.tsx b/src/app/components/ChartBuilderModal.tsx index d2b8be8..c02d106 100644 --- a/src/app/components/ChartBuilderModal.tsx +++ b/src/app/components/ChartBuilderModal.tsx @@ -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() {
{split ? ( <> - Dimensions + {/* The visible count says "there's more below the fold" — the grouped + shelf only appears for wide datasets, where the list scrolls. */} + Dimensions · {dimensions.length}
{dimensions.map(fieldButton)}
- Measures + Measures · {measures.length + 1}
{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 (
@@ -776,14 +782,9 @@ function FilterRow({ filter, columns }: { filter: BuilderFilter; columns: Builde triggerContent={filter.field ? undefined : 'Choose a field…'} /> )} - +
{!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 })} /> - +
@@ -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.

- + ); } @@ -1470,12 +1470,12 @@ export function ChartBuilderModal() {

)}
- - +