mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Chart builder: equal shelf heights, pill floor, flat field rows, scroll affordance
This commit is contained in:
@@ -131,7 +131,8 @@
|
|||||||
.valueInput {
|
.valueInput {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
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: var(--border-width) solid var(--border-strong);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
@@ -149,7 +150,8 @@
|
|||||||
.exprInput {
|
.exprInput {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
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: var(--border-width) solid var(--border-strong);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
@@ -161,7 +163,8 @@
|
|||||||
.calcName {
|
.calcName {
|
||||||
flex: 0 1 110px;
|
flex: 0 1 110px;
|
||||||
min-width: 0;
|
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: var(--border-width) solid var(--border-strong);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
@@ -187,30 +190,7 @@
|
|||||||
border-color: var(--support-error);
|
border-color: var(--support-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove-row × — icon-only, quiet until hovered. */
|
/* Remove-row × is the shared IconButton (sm). */
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Expression escape-hatch toggle — a low-emphasis link, like the hint fixes. */
|
/* Expression escape-hatch toggle — a low-emphasis link, like the hint fixes. */
|
||||||
.modeToggle {
|
.modeToggle {
|
||||||
@@ -450,7 +430,8 @@
|
|||||||
|
|
||||||
.dimInput {
|
.dimInput {
|
||||||
width: 100px;
|
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: var(--border-width) solid var(--border-strong);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
@@ -540,6 +521,7 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cancel / Create are shared Buttons (secondary / primary, lg — modal footer). */
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -547,45 +529,6 @@
|
|||||||
margin-top: auto;
|
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: live preview ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
/* Right side = on-chart shelves stacked above the live preview (spec §06). */
|
/* Right side = on-chart shelves stacked above the live preview (spec §06). */
|
||||||
@@ -694,6 +637,22 @@
|
|||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
max-height: 280px;
|
max-height: 280px;
|
||||||
overflow-y: auto;
|
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
|
/* An armed channel makes the shelf the live target — say so where the next click
|
||||||
@@ -726,33 +685,39 @@
|
|||||||
.shelfList {
|
.shelfList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.shelfField {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-2);
|
||||||
border: var(--border-width) solid var(--border);
|
border: none;
|
||||||
border-radius: var(--radius);
|
background: transparent;
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background var(--dur-fast) var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelfField + .shelfField {
|
||||||
|
border-top: var(--border-width) solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelfField:hover {
|
.shelfField:hover {
|
||||||
border-color: var(--accent);
|
|
||||||
background: var(--layer-01);
|
background: var(--layer-01);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelfField:focus-visible {
|
.shelfField:focus-visible {
|
||||||
outline: 2px solid var(--focus);
|
outline: 2px solid var(--focus);
|
||||||
outline-offset: 1px;
|
outline-offset: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelfFieldUsed {
|
.shelfFieldUsed {
|
||||||
@@ -808,7 +773,12 @@
|
|||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
display: inline-flex;
|
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%;
|
max-width: 100%;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--accent-contrast);
|
color: var(--accent-contrast);
|
||||||
@@ -821,7 +791,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pillType {
|
.pillType {
|
||||||
padding: var(--space-2);
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 var(--space-2);
|
||||||
border: none;
|
border: none;
|
||||||
background: color-mix(in srgb, #000 18%, var(--accent));
|
background: color-mix(in srgb, #000 18%, var(--accent));
|
||||||
color: var(--accent-contrast);
|
color: var(--accent-contrast);
|
||||||
@@ -850,7 +822,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pillTag {
|
.pillTag {
|
||||||
padding: var(--space-2);
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 var(--space-2);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -861,7 +835,8 @@
|
|||||||
/* Grow into any pill slack so the remove ✕ stays at the pill's right edge. */
|
/* Grow into any pill slack so the remove ✕ stays at the pill's right edge. */
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: var(--space-2) var(--space-3);
|
align-self: center;
|
||||||
|
padding: 0 var(--space-3);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -872,7 +847,7 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: var(--space-2);
|
padding: 0 var(--space-2);
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -922,7 +897,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
min-height: 32px;
|
min-height: var(--control-height);
|
||||||
padding: var(--space-1) var(--space-2);
|
padding: var(--space-1) var(--space-2);
|
||||||
border: var(--border-width) dashed var(--border-strong);
|
border: var(--border-width) dashed var(--border-strong);
|
||||||
background: var(--layer-01);
|
background: var(--layer-01);
|
||||||
@@ -999,7 +974,10 @@
|
|||||||
.shelfStrip {
|
.shelfStrip {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
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);
|
border: var(--border-width) solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1052,13 +1030,14 @@
|
|||||||
|
|
||||||
.propInput {
|
.propInput {
|
||||||
width: 200px;
|
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: var(--border-width) solid var(--border-strong);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.propInput:disabled {
|
.propInput:disabled {
|
||||||
@@ -1077,7 +1056,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
min-height: 32px;
|
min-height: var(--control-height);
|
||||||
padding: var(--space-1) var(--space-3);
|
padding: var(--space-1) var(--space-3);
|
||||||
border: var(--border-width) dashed var(--border);
|
border: var(--border-width) dashed var(--border);
|
||||||
color: var(--text-placeholder);
|
color: var(--text-placeholder);
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ import {
|
|||||||
useChartBuilderStore,
|
useChartBuilderStore,
|
||||||
} from '../stores/ChartBuilderStore';
|
} from '../stores/ChartBuilderStore';
|
||||||
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
|
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { IconButton } from './IconButton';
|
||||||
import { SelectControl } from './SelectControl';
|
import { SelectControl } from './SelectControl';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import styles from './ChartBuilderModal.module.css';
|
import styles from './ChartBuilderModal.module.css';
|
||||||
@@ -579,9 +581,11 @@ function FieldShelf() {
|
|||||||
<div className={`${styles.shelfScroll} ${activeChannel ? styles.shelfArmed : ''}`}>
|
<div className={`${styles.shelfScroll} ${activeChannel ? styles.shelfArmed : ''}`}>
|
||||||
{split ? (
|
{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>
|
<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}>
|
<div className={styles.shelfList}>
|
||||||
{measures.map(fieldButton)}
|
{measures.map(fieldButton)}
|
||||||
{countButton}
|
{countButton}
|
||||||
@@ -618,6 +622,8 @@ function MarksCard() {
|
|||||||
|
|
||||||
/** A reserved, non-interactive shelf slot for faceting (small multiples) — Phase 4.
|
/** A reserved, non-interactive shelf slot for faceting (small multiples) — Phase 4.
|
||||||
* Shown so the layout telegraphs where row/column faceting will live. */
|
* 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' }) {
|
function FacetSlot({ kind }: { kind: 'column' | 'row' }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.facetSlot} title="Faceting → small multiples (coming later)">
|
<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…'}
|
triggerContent={filter.field ? undefined : 'Choose a field…'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<button
|
<IconButton size="sm" label="Remove filter" onClick={() => removeFilter(filter.id)}>
|
||||||
type="button"
|
|
||||||
className={styles.removeRow}
|
|
||||||
aria-label="Remove filter"
|
|
||||||
onClick={() => removeFilter(filter.id)}
|
|
||||||
>
|
|
||||||
<Icon name="close" />
|
<Icon name="close" />
|
||||||
</button>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!expressionMode && (
|
{!expressionMode && (
|
||||||
@@ -876,14 +877,13 @@ function CalculateRow({ calc, columns }: { calc: BuilderCalculate; columns: Buil
|
|||||||
aria-describedby={exprMsgId}
|
aria-describedby={exprMsgId}
|
||||||
onChange={(e) => updateCalculate(calc.id, { expr: e.target.value })}
|
onChange={(e) => updateCalculate(calc.id, { expr: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<button
|
<IconButton
|
||||||
type="button"
|
size="sm"
|
||||||
className={styles.removeRow}
|
label="Remove calculated field"
|
||||||
aria-label="Remove calculated field"
|
|
||||||
onClick={() => removeCalculate(calc.id)}
|
onClick={() => removeCalculate(calc.id)}
|
||||||
>
|
>
|
||||||
<Icon name="close" />
|
<Icon name="close" />
|
||||||
</button>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<ExprFeedback expr={calc.expr} columns={columns} messageId={exprMsgId} />
|
<ExprFeedback expr={calc.expr} columns={columns} messageId={exprMsgId} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1303,16 +1303,16 @@ function NoDatasets() {
|
|||||||
The Chart Builder turns a saved dataset into a chart — pick columns, watch the chart take
|
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.
|
shape, and save it as a snippet. Add a dataset to start building.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="primary"
|
||||||
className={`${styles.action} ${styles.primary}`}
|
size="lg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openModal('datasets');
|
openModal('datasets');
|
||||||
useDatasetStore.getState().startCreate();
|
useDatasetStore.getState().startCreate();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add a dataset
|
Add a dataset
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1470,12 +1470,12 @@ export function ChartBuilderModal() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<button type="button" className={styles.action} onClick={() => void closeModal()}>
|
<Button size="lg" onClick={() => void closeModal()}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="primary"
|
||||||
className={`${styles.action} ${styles.primary}`}
|
size="lg"
|
||||||
disabled={!valid}
|
disabled={!valid}
|
||||||
aria-describedby={!valid ? 'cb-create-hint' : undefined}
|
aria-describedby={!valid ? 'cb-create-hint' : undefined}
|
||||||
// The create is the user's confirmation — close with no discard prompt.
|
// The create is the user's confirmation — close with no discard prompt.
|
||||||
@@ -1484,7 +1484,7 @@ export function ChartBuilderModal() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Create Snippet
|
Create Snippet
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user