mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
UX second pass: icon-only header, pane-toggle cue, schema columns, facet cleanup
This commit is contained in:
@@ -70,7 +70,9 @@ export function AboutModal() {
|
||||
yourself.
|
||||
</li>
|
||||
<li>After the first load, the app works fully offline.</li>
|
||||
<li>Use Import / Export to move your library between devices.</li>
|
||||
<li>
|
||||
Use the header’s import / export buttons to move your library between devices.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1007,25 +1007,3 @@
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Reserved faceting placeholder (Phase 4) — non-interactive, signals where it'll live. */
|
||||
.facetSlot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-width: 150px;
|
||||
min-height: var(--control-height);
|
||||
padding: var(--space-1) var(--space-3);
|
||||
border: var(--border-width) dashed var(--border);
|
||||
color: var(--text-placeholder);
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.facetTag {
|
||||
padding: 0 var(--space-2);
|
||||
border: var(--border-width) solid var(--border);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
@@ -620,24 +620,13 @@ 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)">
|
||||
<span>+ {kind} facet</span>
|
||||
<span className={styles.facetTag}>later</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The on-chart **Columns** (X) and **Rows** (Y) shelves stacked above the preview,
|
||||
* Tableau-style — position is a property of the chart, so its controls sit on the
|
||||
* chart. Each shelf holds the axis slot plus a reserved faceting placeholder. A Swap
|
||||
* X/Y action flips the two axes.
|
||||
* chart. A Swap X/Y action flips the two axes. The slot rows are shelf-shaped so
|
||||
* Phase 4 faceting can add a second slot per shelf without a layout change, but
|
||||
* no placeholder ships before the feature does (an affordance promising unbuilt
|
||||
* functionality is a promise, not a feature — NN/g #2/#8).
|
||||
*/
|
||||
function OnChartShelves() {
|
||||
const swapXY = useChartBuilderStore((s) => s.swapXY);
|
||||
@@ -653,14 +642,12 @@ function OnChartShelves() {
|
||||
<span className={styles.shelfName}>Columns</span>
|
||||
<div className={styles.shelfSlots}>
|
||||
<ChannelSlot channel="x" hint="Add a field for the X axis" />
|
||||
<FacetSlot kind="column" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.shelfStrip}>
|
||||
<span className={styles.shelfName}>Rows</span>
|
||||
<div className={styles.shelfSlots}>
|
||||
<ChannelSlot channel="y" hint="Add a field for the Y axis" />
|
||||
<FacetSlot kind="row" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -235,12 +235,17 @@
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Two-up multi-column flow (min 220px per column, so a narrow pane falls back
|
||||
to one): a wide schema reads down-then-over and a 25-field dataset no longer
|
||||
buries the preview table below a full-screen scroll (NN/g #8;
|
||||
docs/ux-second-pass.md resolution 2026-06-13). */
|
||||
.columns {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
columns: 220px 2;
|
||||
column-gap: 0;
|
||||
column-rule: var(--border-width) solid var(--border);
|
||||
border: var(--border-width) solid var(--border);
|
||||
}
|
||||
|
||||
@@ -250,10 +255,11 @@
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
}
|
||||
|
||||
.column + .column {
|
||||
break-inside: avoid;
|
||||
/* Hairline between rows; the negative margin tucks each column's first border
|
||||
under the container border (a `+`-sibling border would double there). */
|
||||
border-top: var(--border-width) solid var(--border);
|
||||
margin-top: calc(-1 * var(--border-width));
|
||||
}
|
||||
|
||||
.columnName {
|
||||
|
||||
@@ -41,10 +41,12 @@ import styles from './LivePreview.module.css';
|
||||
|
||||
/** The four fit modes in display order (spec §04 → Fit / Sizing Modes). */
|
||||
const FIT_OPTIONS: ReadonlyArray<SegmentedOption<FitMode>> = [
|
||||
{ value: 'default', label: 'Original' },
|
||||
{ value: 'width', label: 'Width' },
|
||||
{ value: 'height', label: 'Height' },
|
||||
{ value: 'full', label: 'Full' },
|
||||
// `title` doubles as the accessible name, so it leads with the visible label
|
||||
// (WCAG 2.5.3 label-in-name; see SegmentedOption.title).
|
||||
{ value: 'default', label: 'Original', title: 'Original — the natural size from the spec' },
|
||||
{ value: 'width', label: 'Width', title: 'Width — fit to the pane width' },
|
||||
{ value: 'height', label: 'Height', title: 'Height — fit to the pane height' },
|
||||
{ value: 'full', label: 'Full', title: 'Full — fill the pane' },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -81,9 +83,11 @@ function FitControl() {
|
||||
* snippet's own `config` still overrides it property by property. Lives in the
|
||||
* header, not inside PreviewSettings: SelectControl and SettingsPopover share
|
||||
* the one-open-popover registry, so a select nested in the popover would close
|
||||
* (and unmount) its own parent on open.
|
||||
* (and unmount) its own parent on open (the contract: arch 10 §5 — a control
|
||||
* needing its own popover sits beside the gear, never inside the panel). The
|
||||
* trigger's 16ch label cap keeps long preset/custom names from crowding a
|
||||
* narrow header.
|
||||
*/
|
||||
// TODO: header placement/crowding parked for the batched council pass (docs/ux-second-pass.md).
|
||||
/** Sentinel option that opens the Theme Builder instead of selecting a theme. */
|
||||
const EDIT_THEMES = 'edit-themes';
|
||||
type ThemePickerValue = ChartThemeSelection | typeof EDIT_THEMES;
|
||||
@@ -103,14 +107,16 @@ function ChartThemeControl() {
|
||||
// the entries it manages. The roster boundary itself (the divider) is set by
|
||||
// chartThemeOptions where the order is decided, so the splice is the only
|
||||
// index this component owns. The first preset is the divider-carrying option,
|
||||
// so splicing right before it needs no count arithmetic.
|
||||
// TODO: action row inside a value picker (visual separation? AT surprise?)
|
||||
// parked for the batched council pass (docs/ux-second-pass.md).
|
||||
// so splicing right before it needs no count arithmetic. The row needs no
|
||||
// special styling: SelectControl options are real buttons (not listbox
|
||||
// options), the "…" is the opens-further-UI convention, and `hasPopup`
|
||||
// announces the dialog to AT.
|
||||
const firstPreset = list.findIndex((o) => o.dividerBefore);
|
||||
list.splice(firstPreset === -1 ? list.length : firstPreset, 0, {
|
||||
value: EDIT_THEMES,
|
||||
label: 'Edit themes…',
|
||||
detail: 'Create and manage custom themes',
|
||||
hasPopup: 'dialog',
|
||||
});
|
||||
return list;
|
||||
}, [customThemes]);
|
||||
|
||||
@@ -17,14 +17,17 @@
|
||||
}
|
||||
|
||||
/* The controls are shared IconButtons; only the toggle-on state is local.
|
||||
Shown pane: the toggle is "on" — full-strength glyph on a filled chip.
|
||||
Shown pane: the toggle is "on" — full-strength glyph on a filled chip plus an
|
||||
accent edge bar (the activity-bar cue). The bar is the state cue the fill
|
||||
alone can't be: hover uses the same --layer-02 fill, so a fill-only pressed
|
||||
state is indistinguishable from hovering an off toggle (arch 09: state
|
||||
indication needs its own 3:1 cue, never a hover collision). An inset bar,
|
||||
not a border — boxless per the arch 09 box-discipline rule.
|
||||
(.strip prefix outranks the primitive's base background/color.) */
|
||||
/* TODO: the pressed chip (--layer-02 fill) is identical to the IconButton hover
|
||||
fill on this surface, so hovering an off toggle looks the same as an on toggle
|
||||
at rest — the pressed state needs its own cue (docs/ux-second-pass.md). */
|
||||
.strip .pressed {
|
||||
color: var(--text);
|
||||
background: var(--layer-02);
|
||||
box-shadow: inset 3px 0 0 var(--accent);
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
||||
@@ -42,6 +42,10 @@
|
||||
|
||||
.triggerLabel {
|
||||
min-width: 0;
|
||||
/* Cap the trigger so a long value (a preset or user theme name) can't blow
|
||||
out a crowded pane header at narrow widths; the full label stays available
|
||||
in the open list and the trigger's aria-label (arch 10 §5). */
|
||||
max-width: 16ch;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,12 @@ export interface SelectControlOption<V extends string> {
|
||||
* the user's themes and the preset roster.
|
||||
*/
|
||||
dividerBefore?: boolean;
|
||||
/**
|
||||
* For an action row that opens a dialog instead of picking a value (e.g. the
|
||||
* theme picker's "Edit themes…"): sets `aria-haspopup` so AT announces that
|
||||
* activating discloses further UI, matching the "…" label convention.
|
||||
*/
|
||||
hasPopup?: 'dialog';
|
||||
}
|
||||
|
||||
export interface SelectControlProps<V extends string> {
|
||||
@@ -162,6 +168,7 @@ export function SelectControl<V extends string>({
|
||||
type="button"
|
||||
className={`${styles.option} ${selected ? styles.selected : ''}`}
|
||||
aria-current={selected || undefined}
|
||||
aria-haspopup={o.hasPopup}
|
||||
onClick={() => choose(o.value)}
|
||||
>
|
||||
<span className={styles.optionLabel}>
|
||||
|
||||
@@ -233,9 +233,10 @@ export function ThemeBuilderModal() {
|
||||
<label className={styles.label} htmlFor="theme-config">
|
||||
Config (Vega-Lite JSON)
|
||||
</label>
|
||||
{/* TODO: plain textarea vs a Monaco instance (config-schema completions), and
|
||||
text alternatives for the canvas gallery beyond the captions — parked for
|
||||
the batched council pass (docs/ux-second-pass.md). */}
|
||||
{/* A plain textarea, deliberately not Monaco: a second Monaco mount
|
||||
is heavy inside a modal for an occasional surface, and the parse
|
||||
error below is the feedback channel that matters here. Revisit
|
||||
only if real usage asks for config completions. */}
|
||||
<textarea
|
||||
id="theme-config"
|
||||
className={styles.configText}
|
||||
|
||||
Reference in New Issue
Block a user