Chart builder: filter/calculate transforms, data preview, and inline expression validation

This commit is contained in:
2026-06-11 13:01:23 +03:00
parent 1791ee9f8d
commit 05df0cd7d3
13 changed files with 1919 additions and 27 deletions
@@ -43,6 +43,331 @@
color: var(--text);
}
/* ── Data section: filters, calculated fields, row preview (spec §06 → Data) ──── */
.dataSection {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.transformGroup {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.transformGroupHead {
display: flex;
align-items: center;
justify-content: space-between;
}
/* "Add filter" / "Add field" — a low-emphasis ghost action, like Swap X/Y. */
.addRow {
display: inline-flex;
align-items: center;
gap: 4px;
border: none;
background: transparent;
color: var(--accent);
font: inherit;
font-size: 12px;
cursor: pointer;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius);
}
.addRow:hover {
background: var(--layer-01);
}
.addRow:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 1px;
}
/* A filter / calculated-field row, bordered like a channel block for visual rhyme. */
.transformBlock {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-3);
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
}
.transformBlockTop {
display: flex;
align-items: center;
gap: var(--space-2);
}
.filterField {
flex: 1;
min-width: 0;
padding: var(--space-2) var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 13px;
}
.filterPredicate {
display: flex;
align-items: center;
gap: var(--space-2);
}
.filterPredicate .mini {
flex: none;
}
.valueInput {
flex: 1;
min-width: 0;
padding: var(--space-1) 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: 13px;
}
.rangeDash {
flex: none;
color: var(--text-placeholder);
}
/* Expression inputs (filter expression / calculate) — monospace, fill the row. */
.exprInput {
flex: 1;
min-width: 0;
padding: var(--space-2) var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font-family: var(--font-mono);
font-size: 12px;
}
.calcName {
flex: 0 1 110px;
min-width: 0;
padding: var(--space-2) var(--space-3);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font-family: var(--font-mono);
font-size: 12px;
}
.calcEquals {
flex: none;
color: var(--text-placeholder);
font-family: var(--font-mono);
}
.filterField:focus-visible,
.calcName:focus-visible,
.valueInput:focus-visible,
.exprInput:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
}
.exprInput[aria-invalid='true'] {
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;
}
/* Expression escape-hatch toggle — a low-emphasis link, like the hint fixes. */
.modeToggle {
align-self: flex-start;
border: none;
background: transparent;
color: var(--accent);
font: inherit;
font-size: 11px;
cursor: pointer;
padding: 2px var(--space-1);
border-radius: var(--radius);
}
.modeToggle:hover {
background: var(--layer-01);
}
.modeToggle:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 1px;
}
.exprError,
.exprWarn {
margin: 0;
font-size: 11px;
line-height: 1.4;
}
.exprError {
color: var(--support-error);
}
.exprWarn {
color: var(--support-warning-fg);
}
/* Contextual pointer to the Vega expression docs (shown when an expression exists). */
.exprHelp {
margin: 0;
font-size: 11px;
line-height: 1.4;
color: var(--text-secondary);
}
.exprHelpLink {
color: var(--accent);
text-decoration: none;
border-radius: var(--radius);
}
.exprHelpLink:hover {
text-decoration: underline;
}
.exprHelpLink:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 2px;
}
/* Row preview disclosure (1D). */
.dataPreview {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.previewToggle {
display: inline-flex;
align-items: center;
gap: var(--space-2);
align-self: flex-start;
border: none;
background: transparent;
color: var(--text-secondary);
font: inherit;
font-size: 12px;
cursor: pointer;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius);
}
.previewToggle:hover {
background: var(--layer-01);
}
.previewToggle:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 1px;
}
.previewCaret {
font-size: 10px;
color: var(--text-placeholder);
}
.previewMeta {
color: var(--text-placeholder);
}
.previewTableWrap {
max-height: 200px;
overflow: auto;
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
}
.previewTableWrap:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -1px;
}
.previewTable {
border-collapse: collapse;
width: 100%;
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.4;
}
.previewTable th,
.previewTable td {
text-align: left;
padding: var(--space-1) var(--space-2);
border-bottom: var(--border-width) solid var(--border);
white-space: nowrap;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
}
.previewTable thead th {
position: sticky;
top: 0;
z-index: 1;
background: var(--layer-01);
}
.previewColName {
font-weight: 600;
color: var(--text-secondary);
}
.previewColType {
color: var(--text-placeholder);
font-weight: 400;
}
.previewTable tbody tr:last-child td {
border-bottom: none;
}
.previewEmptyNote {
margin: 0;
font-size: 11px;
color: var(--text-secondary);
}
.field {
display: flex;
flex-direction: column;