mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
1063 lines
23 KiB
CSS
1063 lines
23 KiB
CSS
/* Chart Builder — two-pane modal body (spec §06). */
|
||
|
||
.builder {
|
||
display: grid;
|
||
grid-template-columns: minmax(320px, 360px) 1fr;
|
||
/* Fill the modal body and never exceed it, so each pane scrolls internally rather
|
||
than the whole modal growing past the viewport (otherwise a tall chart pushes the
|
||
Create/Cancel actions below the fold). The `minmax(0, 1fr)` row lets the panes
|
||
shrink below their content height so their own overflow kicks in. */
|
||
grid-template-rows: minmax(0, 1fr);
|
||
height: 100%;
|
||
min-height: 0;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* ── No-dataset states: Carbon no-data empty state / dataset chooser ──── */
|
||
|
||
.emptyState {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: var(--space-4);
|
||
max-width: 48ch;
|
||
padding: var(--space-7) var(--space-6);
|
||
}
|
||
|
||
.emptyTitle {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
.emptyBody {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* ── Left: configuration ─────────────────────────────────────────────── */
|
||
|
||
.configPane {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-5);
|
||
padding: var(--space-5);
|
||
border-right: var(--border-width) solid var(--border);
|
||
overflow-y: auto;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* The dataset picker pinned at the top of the config pane. */
|
||
.datasetRow {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-3);
|
||
}
|
||
|
||
/* Edit-in-place banner (spec §06 → Open in builder): names the snippet the builder
|
||
is editing, so the primary "Save changes" action reads unambiguously. */
|
||
.editingBanner {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.editingBanner strong {
|
||
color: var(--text-primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* ── Intent front door (spec §06 → Intent) ───────────────────────────────
|
||
A persistent "what do you want to show?" strip under the dataset picker. A
|
||
quiet accent-soft wash marks it as the guided on-ramp without competing with
|
||
the primary Create action (--accent stays reserved for primary actions). */
|
||
.intentStrip {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
padding: var(--space-4);
|
||
border: var(--border-width) solid var(--border);
|
||
background: var(--accent-soft);
|
||
}
|
||
|
||
.intentQ {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
.intentSub {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.intentChips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--space-2);
|
||
margin-top: var(--space-1);
|
||
}
|
||
|
||
.intentChip {
|
||
appearance: none;
|
||
height: var(--control-height);
|
||
border: var(--border-width) solid var(--border-strong);
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font: inherit;
|
||
font-size: 12px;
|
||
padding: 0 var(--space-3);
|
||
cursor: pointer;
|
||
transition:
|
||
background var(--dur-fast) var(--ease),
|
||
box-shadow var(--dur-fast) var(--ease);
|
||
}
|
||
|
||
.intentChip:hover {
|
||
background: var(--field);
|
||
}
|
||
|
||
/* The chip whose layout the live chart matches (council-reserved: selection uses an
|
||
accent ring, never an accent fill — fill stays the primary-action signal). */
|
||
.intentChip[aria-pressed='true'] {
|
||
border-color: var(--accent);
|
||
box-shadow: inset 0 0 0 1px var(--accent);
|
||
}
|
||
|
||
/* Disabled = the dataset can't satisfy this intent: perceivable but inert (the chip's
|
||
accessible name carries the reason). */
|
||
.intentChip[aria-disabled='true'] {
|
||
color: var(--text-placeholder);
|
||
border-color: var(--border);
|
||
background: var(--bg);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.intentChip:focus-visible {
|
||
outline: 2px solid var(--focus);
|
||
outline-offset: 1px;
|
||
}
|
||
|
||
/* ── 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);
|
||
}
|
||
|
||
.filterPredicate {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
.filterPredicate .mini {
|
||
flex: none;
|
||
}
|
||
|
||
.valueInput {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: var(--control-height);
|
||
padding: 0 var(--space-2);
|
||
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;
|
||
height: var(--control-height);
|
||
padding: 0 var(--space-3);
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.calcName {
|
||
flex: 0 1 110px;
|
||
min-width: 0;
|
||
height: var(--control-height);
|
||
padding: 0 var(--space-3);
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.calcEquals {
|
||
flex: none;
|
||
color: var(--text-placeholder);
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
.exprInput[aria-invalid='true'] {
|
||
border-color: var(--support-error);
|
||
}
|
||
|
||
/* Remove-row × is the shared IconButton (sm). */
|
||
|
||
/* 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 {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: var(--space-2);
|
||
margin: 0;
|
||
font-size: 11px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* Severity reads from the glyph shape + colour, not colour alone (arch 10 §3); the
|
||
icon inherits the line's colour via currentColor (round error / triangle warning). */
|
||
.exprFeedbackIcon {
|
||
flex: none;
|
||
margin-top: 1px;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
/* The per-column type chip in the source-preview header (the table structure and
|
||
cell styling now live in the shared DataTable). */
|
||
.previewColName {
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.previewColType {
|
||
color: var(--text-placeholder);
|
||
font-weight: 400;
|
||
}
|
||
|
||
.previewEmptyNote {
|
||
margin: 0;
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
/* The mark picker carries six segments — too many for one 320–360px row. Override the
|
||
SegmentedControl's fixed single-row height so it wraps; each option keeps the control
|
||
height so the two rows read as even segments inside the shared border. */
|
||
.markPicker {
|
||
flex-wrap: wrap;
|
||
height: auto;
|
||
}
|
||
|
||
.markPickerOption {
|
||
min-height: var(--control-height);
|
||
}
|
||
|
||
.fieldLabel {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.swap {
|
||
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);
|
||
}
|
||
|
||
.swap:hover {
|
||
background: var(--layer-01);
|
||
}
|
||
|
||
.swap:focus-visible {
|
||
outline: 2px solid var(--focus);
|
||
outline-offset: 1px;
|
||
}
|
||
|
||
/* Channel name label (Marks card rows + the on-chart Columns/Rows shelves). */
|
||
.channelLabel {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
.transform {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.miniLabel {
|
||
font-size: 11px;
|
||
color: var(--text-placeholder);
|
||
}
|
||
|
||
.toggle {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.chartControls {
|
||
display: flex;
|
||
gap: var(--space-5);
|
||
}
|
||
|
||
.dimInput {
|
||
width: 100px;
|
||
height: var(--control-height);
|
||
padding: 0 var(--space-3);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.warnings {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
margin: 0;
|
||
padding: var(--space-3);
|
||
list-style: none;
|
||
background: var(--layer-01);
|
||
border: var(--border-width) solid var(--border);
|
||
border-radius: var(--radius);
|
||
}
|
||
|
||
/* These take focus only programmatically (tabIndex -1) after a hint fix is applied,
|
||
to keep focus off <body>; no visible ring for that script-driven move. */
|
||
.warnings:focus,
|
||
.configPane:focus {
|
||
outline: none;
|
||
}
|
||
|
||
.warning {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: var(--space-2);
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* Filled status-warning glyph (arch 09 §5.2), in the contrast-safe amber. Replaces
|
||
the old ⚠ text character so the warning matches the toast status family. */
|
||
.warningIcon {
|
||
flex: none;
|
||
margin-top: 1px;
|
||
color: var(--support-warning-fg);
|
||
}
|
||
|
||
/* The hint text and its one-click remedies stacked, growing beside the icon. */
|
||
.warningBody {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
min-width: 0;
|
||
}
|
||
|
||
/* Actionable-hint remedies: each fix is an offer, never forced — low-emphasis
|
||
suggestions beside the advice, not commands. */
|
||
.warningFixes {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
/* Inline link-style actions (arch 09 §4): content-sized accent text, no box —
|
||
the smallness is the emphasis level. */
|
||
.warningFix {
|
||
font: inherit;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
padding: var(--space-1) var(--space-2);
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
background: transparent;
|
||
color: var(--accent);
|
||
}
|
||
|
||
.warningFix:hover {
|
||
background: var(--control-hover-fill, var(--layer-01));
|
||
}
|
||
|
||
.warningFix:focus-visible {
|
||
outline: 2px solid var(--focus);
|
||
outline-offset: 1px;
|
||
}
|
||
|
||
/* Explains the disabled Create action (contract 10: a disabled control must say
|
||
why). `margin-top: auto` pins it just above the actions so the two read as one. */
|
||
.createHint {
|
||
margin: auto 0 0;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
text-align: right;
|
||
}
|
||
|
||
/* Cancel / Create are shared Buttons (secondary / primary, lg — modal footer). */
|
||
.actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: var(--space-3);
|
||
margin-top: auto;
|
||
}
|
||
|
||
/* ── Right: live preview ─────────────────────────────────────────────── */
|
||
|
||
/* Right side = on-chart shelves stacked above the live preview (spec §06). */
|
||
.previewSide {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
background: var(--bg);
|
||
}
|
||
|
||
.previewPane {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
padding: var(--space-5);
|
||
min-width: 0;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
background: var(--bg);
|
||
}
|
||
|
||
.previewHint {
|
||
margin: auto;
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
text-align: center;
|
||
}
|
||
|
||
.previewFrame {
|
||
flex: 1;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
display: flex;
|
||
/* A chart taller/wider than the pane scrolls *here*, inside a fixed viewport, so
|
||
the modal keeps its shape. `safe` centring aligns to the start instead of
|
||
clipping the top/left when the chart overflows. */
|
||
overflow: auto;
|
||
align-items: safe center;
|
||
justify-content: safe center;
|
||
}
|
||
|
||
.previewHost {
|
||
width: 100%;
|
||
}
|
||
|
||
.previewError {
|
||
margin: auto 0;
|
||
padding: var(--space-3);
|
||
}
|
||
|
||
.previewErrorHeadline {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: var(--support-error);
|
||
}
|
||
|
||
.previewErrorDetails {
|
||
margin-top: var(--space-2);
|
||
}
|
||
|
||
.previewErrorSummary {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.previewErrorDetail {
|
||
margin: var(--space-2) 0 0;
|
||
padding: var(--space-3);
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
color: var(--text);
|
||
background: var(--layer-01);
|
||
border: var(--border-width) solid var(--border);
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
/* ── Encoding: field shelf + Tableau-style shelves/pills (spec §06 → Encoding) ─── */
|
||
|
||
/* Field shelf — the dataset's columns as clickable, type-glyphed chips (field-first). */
|
||
.fieldShelf {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
.shelfGroupHead {
|
||
margin-top: var(--space-2);
|
||
font-size: 11px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
/* Scroll viewport around the field lists: a wide dataset (Superstore: 25 columns)
|
||
must not push the Marks card and chart controls below the fold. Group headings
|
||
pin to the top while their list scrolls under them. */
|
||
.shelfScroll {
|
||
display: flex;
|
||
flex-direction: column;
|
||
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
|
||
happens (NN/g #1): an accent ring on the list plus the status line above it. */
|
||
.shelfArmed {
|
||
outline: 2px solid var(--accent);
|
||
outline-offset: 2px;
|
||
border-radius: var(--radius);
|
||
}
|
||
|
||
.armedHint {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.armedHint strong {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.shelfScroll .shelfGroupHead {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1;
|
||
margin-top: 0;
|
||
padding: var(--space-2) 0;
|
||
background: var(--layer-01);
|
||
}
|
||
|
||
.shelfList {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 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-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 {
|
||
background: var(--layer-01);
|
||
}
|
||
|
||
.shelfField:focus-visible {
|
||
outline: 2px solid var(--focus);
|
||
outline-offset: -1px;
|
||
}
|
||
|
||
.shelfFieldUsed {
|
||
opacity: 0.55;
|
||
}
|
||
|
||
.shelfGlyph {
|
||
min-width: 22px;
|
||
padding: 1px var(--space-2);
|
||
/* A passive type tag, not a control — subtle border (borders mark function). */
|
||
border: var(--border-width) solid var(--border);
|
||
background: var(--layer-01);
|
||
color: var(--text-secondary);
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
text-align: center;
|
||
}
|
||
|
||
.shelfFieldName {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Marks card — the non-positional encodings (Colour, Size). */
|
||
.marksCard {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-3);
|
||
padding: var(--space-3);
|
||
border: var(--border-width) solid var(--border);
|
||
border-radius: var(--radius);
|
||
}
|
||
|
||
.marksRow {
|
||
display: grid;
|
||
grid-template-columns: 44px 1fr;
|
||
align-items: start;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
/* Pill — a bound channel (field/count or constant). The per-channel transform
|
||
controls (Aggregate/Bin/Granularity) flow inline beside the pill, wrapping only
|
||
when the row truly runs out of room (a narrow window; the Marks card). */
|
||
.pillWrap {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: var(--space-2) var(--space-3);
|
||
min-width: 0;
|
||
}
|
||
|
||
.pill {
|
||
display: inline-flex;
|
||
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);
|
||
border-radius: var(--radius);
|
||
}
|
||
|
||
.pillConst {
|
||
background: var(--layer-02);
|
||
color: var(--text);
|
||
}
|
||
|
||
.pillType {
|
||
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);
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.pillType:hover:not(:disabled) {
|
||
background: color-mix(in srgb, #000 30%, var(--accent));
|
||
}
|
||
|
||
.pillType:disabled {
|
||
cursor: default;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.pillType[aria-expanded='true'] {
|
||
background: color-mix(in srgb, #000 30%, var(--accent));
|
||
}
|
||
|
||
.pillType:focus-visible {
|
||
outline: 2px solid var(--accent-contrast);
|
||
outline-offset: -2px;
|
||
}
|
||
|
||
.pillTag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 0 var(--space-2);
|
||
color: var(--text-secondary);
|
||
font-size: 10px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
|
||
.pillName {
|
||
/* Grow into any pill slack so the remove ✕ stays at the pill's right edge. */
|
||
flex: 1;
|
||
min-width: 0;
|
||
align-self: center;
|
||
padding: 0 var(--space-3);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.pillRemove {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 var(--space-2);
|
||
border: none;
|
||
background: transparent;
|
||
color: inherit;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.pillRemove:hover {
|
||
background: color-mix(in srgb, var(--text) 14%, transparent);
|
||
}
|
||
|
||
.pillRemove:focus-visible {
|
||
outline: 2px solid var(--focus);
|
||
outline-offset: -2px;
|
||
}
|
||
|
||
/* Sizing + chrome come from ColorField (size="sm"); only the pill margin is local. */
|
||
.constColor {
|
||
margin: 0 var(--space-2);
|
||
}
|
||
|
||
.constNumber {
|
||
width: 64px;
|
||
margin: 0 var(--space-2);
|
||
padding: var(--space-1) var(--space-2);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.pillControls {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
/* Empty assign target — armed by click, then filled by a clicked field. */
|
||
.slot {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
min-height: var(--control-height);
|
||
padding: var(--space-1) var(--space-2);
|
||
border: var(--border-width) dashed var(--border-strong);
|
||
background: var(--layer-01);
|
||
}
|
||
|
||
.slotActive {
|
||
border-style: solid;
|
||
border-color: var(--accent);
|
||
box-shadow: inset 0 0 0 1px var(--accent);
|
||
}
|
||
|
||
.slotAssign {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: var(--space-1);
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-placeholder);
|
||
font: inherit;
|
||
font-size: 12px;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.slotAssign:hover {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.slotAssign:focus-visible {
|
||
outline: 2px solid var(--focus);
|
||
outline-offset: 1px;
|
||
}
|
||
|
||
/* Ghost button (Carbon: links navigate, buttons act — this one acts). */
|
||
.slotConst {
|
||
padding: var(--space-1) var(--space-2);
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
background: none;
|
||
color: var(--accent);
|
||
font: inherit;
|
||
font-size: 11px;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
transition: background var(--dur-fast) var(--ease);
|
||
}
|
||
|
||
.slotConst:hover {
|
||
background: var(--layer-02);
|
||
color: var(--accent-hover);
|
||
}
|
||
|
||
.slotConst:focus-visible {
|
||
outline: 2px solid var(--focus);
|
||
outline-offset: 1px;
|
||
}
|
||
|
||
/* On-chart Columns / Rows shelves (X/Y), stacked above the preview. */
|
||
.shelves {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-3);
|
||
padding: var(--space-5) var(--space-5) 0;
|
||
}
|
||
|
||
/* Swap sits beside the heading — an action flushed to the far edge of a very wide
|
||
pane reads as unrelated to the shelves it acts on. */
|
||
.shelvesHead {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: var(--space-4);
|
||
}
|
||
|
||
.shelfStrip {
|
||
display: flex;
|
||
align-items: stretch;
|
||
/* 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);
|
||
}
|
||
|
||
.shelfName {
|
||
flex: 0 0 84px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 var(--space-3);
|
||
border-right: var(--border-width) solid var(--border);
|
||
background: var(--layer-01);
|
||
color: var(--text-secondary);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.shelfSlots {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: var(--space-3);
|
||
min-width: 0;
|
||
padding: var(--space-2) var(--space-3);
|
||
}
|
||
|
||
/* Empty slots keep a minimum presence; a filled pill hugs its content so the
|
||
remove ✕ sits at its right edge rather than adrift in dead pill background. */
|
||
.shelfSlots .slot {
|
||
min-width: 190px;
|
||
}
|
||
|
||
/* Chart properties — title/subtitle/size as a slim strip pinned under the preview
|
||
(chart-level properties live on the chart side; council 2026-06-12). */
|
||
.chartProps {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: var(--space-4);
|
||
padding: var(--space-3) var(--space-5);
|
||
border-top: var(--border-width) solid var(--border);
|
||
}
|
||
|
||
.propField {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.propInput {
|
||
width: 200px;
|
||
height: var(--control-height);
|
||
padding: 0 var(--space-2);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.propInput:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|