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:
@@ -210,8 +210,11 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
|
|||||||
action is filled. Worked examples: the **editor toolbar** (Publish is the lone
|
action is filled. Worked examples: the **editor toolbar** (Publish is the lone
|
||||||
primary; Extract/Revert are secondary, and collapse to icons when narrow — see
|
primary; Extract/Revert are secondary, and collapse to icons when narrow — see
|
||||||
[arch 10 §8](10-interaction-and-feedback.md)), and the **header** (Datasets /
|
[arch 10 §8](10-interaction-and-feedback.md)), and the **header** (Datasets /
|
||||||
Import / Export / About are ghost; a divider then sets off the soft-accent Donate
|
Import / Export / About are **icon-only** IconButtons per Carbon's UI-shell
|
||||||
and the ghost theme toggle).
|
header — global actions as a right-aligned icon row — with accessible names
|
||||||
|
that carry scope, e.g. "Export workspace" vs the preview's per-chart "Export";
|
||||||
|
a divider then sets off the soft-accent Donate, which keeps its text label —
|
||||||
|
a solicitation needs the word — and the ghost theme toggle).
|
||||||
- **Hover is variant-specific:** filled buttons (primary/danger) **darken**
|
- **Hover is variant-specific:** filled buttons (primary/danger) **darken**
|
||||||
(`--accent-hover` / a slight brightness drop); outlined/ghost buttons **gain a
|
(`--accent-hover` / a slight brightness drop); outlined/ghost buttons **gain a
|
||||||
fill one elevation step above their surface** — on `--bg` → `--layer-01`, on a
|
fill one elevation step above their surface** — on `--bg` → `--layer-01`, on a
|
||||||
|
|||||||
@@ -231,6 +231,13 @@ last-focused control on re-entry.
|
|||||||
- The **Datasets** control is a plain **command button** (no `aria-pressed`) in the _same_
|
- The **Datasets** control is a plain **command button** (no `aria-pressed`) in the _same_
|
||||||
toolbar — APG permits mixed control types — set off from the toggles by a visual divider (and
|
toolbar — APG permits mixed control types — set off from the toggles by a visual divider (and
|
||||||
optionally a nested `role="group"`), but kept in the roving sequence as its last element.
|
optionally a nested `role="group"`), but kept in the roving sequence as its last element.
|
||||||
|
- The **pressed (pane-visible) state has its own visual cue**: an inset accent edge bar plus the
|
||||||
|
full-strength glyph on the filled chip (the activity-bar convention). A fill alone cannot be
|
||||||
|
the state cue — hover uses the same one-step fill, so a fill-only pressed state is
|
||||||
|
indistinguishable from hovering an off toggle (the collision class arch 09 names for hover);
|
||||||
|
and a bar, not a border, keeps the strip boxless per the arch 09 box-discipline rule. The
|
||||||
|
unlabeled glyph rail itself is the established activity-bar convention (NN/g #4) — toggles
|
||||||
|
carry tooltips per Carbon's icon-button rule; no visible labels.
|
||||||
- **Focus**: show/hide is only ever initiated **from the strip**, so the activating toggle
|
- **Focus**: show/hide is only ever initiated **from the strip**, so the activating toggle
|
||||||
already holds focus when its pane disappears and **retains it** (the button stays, flips to
|
already holds focus when its pane disappears and **retains it** (the button stays, flips to
|
||||||
not-pressed) — no orphaned focus, no restoration logic. The strip is **never itself hidden**,
|
not-pressed) — no orphaned focus, no restoration logic. The strip is **never itself hidden**,
|
||||||
@@ -252,7 +259,10 @@ the Draft/Published view) is a **radio group**, never a row of `aria-pressed` to
|
|||||||
**toggle switch** was also weighed and rejected: APG defines `role="switch"` as on/off of a
|
**toggle switch** was also weighed and rejected: APG defines `role="switch"` as on/off of a
|
||||||
**single** setting, but Draft/Published selects between two **named peer views** with no
|
**single** setting, but Draft/Published selects between two **named peer views** with no
|
||||||
natural "on" side — a radio group is the right semantics. Reserve the switch for genuine
|
natural "on" side — a radio group is the right semantics. Reserve the switch for genuine
|
||||||
on/off settings. Consulted via /council → APG switch / radio-group / tabs.)_
|
on/off settings. Consulted via /council → APG switch / radio-group / tabs.)_ A per-option
|
||||||
|
`title` (tooltip for a terse label) doubles as the option's accessible name, so it must
|
||||||
|
**lead with the visible label** ("Original — the natural size from the spec"), or
|
||||||
|
speech-input users can't address the control they see (WCAG 2.5.3 label-in-name).
|
||||||
|
|
||||||
**Resolved — selectable lists.** A row the user selects must be a real `<button>` (or a
|
**Resolved — selectable lists.** A row the user selects must be a real `<button>` (or a
|
||||||
proper option), not a click handler on `<li>` (mouse-only, no keyboard, no role). It is
|
proper option), not a click handler on `<li>` (mouse-only, no keyboard, no role). It is
|
||||||
@@ -453,6 +463,14 @@ in the keyboard order, never a heading. **The boundary is set where the list is
|
|||||||
module that decides the option order marks the divider-carrying option (e.g.
|
module that decides the option order marks the divider-carrying option (e.g.
|
||||||
`chartThemeOptions` stamps the first preset); a consumer must never recompute a group
|
`chartThemeOptions` stamps the first preset); a consumer must never recompute a group
|
||||||
boundary by index arithmetic, which silently misplaces when the producer's ordering changes.
|
boundary by index arithmetic, which silently misplaces when the producer's ordering changes.
|
||||||
|
A value list may carry an **action row** (the VS Code theme-picker pattern — e.g. "Edit
|
||||||
|
themes…" inside the chart-theme picker): permissible because options are real buttons, not
|
||||||
|
listbox options (APG's no-interactive-children listbox constraint doesn't apply); the row's
|
||||||
|
label ends in "…" (the opens-further-UI convention) and sets the option's `hasPopup` so AT
|
||||||
|
hears `aria-haspopup` — no special visual styling beyond an adjacent group divider. The
|
||||||
|
default trigger caps its value label at **16ch with ellipsis**, so a long value (a preset or
|
||||||
|
user-named theme) can't blow out a crowded pane header; the full label remains in the open
|
||||||
|
list and the trigger's accessible name.
|
||||||
The single-open registry means **disclosures cannot nest**: a SelectControl inside a
|
The single-open registry means **disclosures cannot nest**: a SelectControl inside a
|
||||||
settings popover would close — and unmount — its own parent on open. A control that needs
|
settings popover would close — and unmount — its own parent on open. A control that needs
|
||||||
its own popover sits beside the gear in the pane header, never inside the panel.
|
its own popover sits beside the gear in the pane header, never inside the panel.
|
||||||
@@ -494,6 +512,15 @@ Datasets (⌘/Ctrl+K), or check the dataset name in your spec."_ — instead of
|
|||||||
must match the actual cause (NN/g #9, GOV.UK error-message). The thrown
|
must match the actual cause (NN/g #9, GOV.UK error-message). The thrown
|
||||||
`DatasetNotFoundError` carries `datasetName` so the surface can name it.
|
`DatasetNotFoundError` carries `datasetName` so the surface can name it.
|
||||||
|
|
||||||
|
**Resolved — no affordance for unbuilt features.** A visible placeholder promising future
|
||||||
|
functionality (the Chart Builder's dashed "+ row facet · LATER" shelf slots, removed
|
||||||
|
2026-06-13) is roadmap language shipped to users: it speaks our planning vocabulary, not
|
||||||
|
theirs (NN/g #2), and competes with the working controls around it (NN/g #8). A gated or
|
||||||
|
later-phase feature gets **no placeholder, tag, or disabled stub** until it ships — disabled
|
||||||
|
states are for _temporarily unavailable_ actions, not unbuilt ones. Design the layout so the
|
||||||
|
future control can land without rework (e.g. shelf slots are row-shaped), and keep the
|
||||||
|
roadmap in `docs/`, not in the UI.
|
||||||
|
|
||||||
**Resolved — service-worker update prompt & persistent storage (web.dev seat).** The build
|
**Resolved — service-worker update prompt & persistent storage (web.dev seat).** The build
|
||||||
uses `registerType: 'prompt'`, so a new service worker waits and never takes over a running
|
uses `registerType: 'prompt'`, so a new service worker waits and never takes over a running
|
||||||
session on its own — the app **must** tell the user, or "ask before updating" silently means
|
session on its own — the app **must** tell the user, or "ask before updating" silently means
|
||||||
|
|||||||
@@ -25,18 +25,19 @@ Behavior:
|
|||||||
A fixed header spans the top of the app.
|
A fixed header spans the top of the app.
|
||||||
|
|
||||||
- **Left side**: the app icon, the app title ("Astrolabe"), and a version badge showing the current app version.
|
- **Left side**: the app icon, the app title ("Astrolabe"), and a version badge showing the current app version.
|
||||||
- **Right side**: a row of text entry points. Each opens a destination:
|
- **Right side**: a row of **icon-only** utility entry points (each with a tooltip and an accessible name), then — set off by a divider — the text-labelled Donate button and the theme toggle. Each opens a destination:
|
||||||
|
|
||||||
| Entry point | Opens |
|
| Entry point | Opens |
|
||||||
| ----------- | -------------------------------------------------------------------------------------------------------------------- |
|
| ---------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||||
| Import | A file-picker dialog to choose a previously exported file; the chosen file is imported (see _Import & Export_). |
|
|
||||||
| Export | Immediately produces a downloaded file containing all snippets and datasets (see _Import & Export_). |
|
|
||||||
| Datasets | The Datasets manager modal (see _Datasets_). |
|
| Datasets | The Datasets manager modal (see _Datasets_). |
|
||||||
| About | The About & Help modal (keyboard shortcuts, about, and privacy information). Labelled "About"; privacy lives inside. |
|
| Import workspace | A file-picker dialog to choose a previously exported file; the chosen file is imported (see _Import & Export_). |
|
||||||
| Donate | The Donate modal. |
|
| Export workspace | Immediately produces a downloaded file containing all snippets and datasets (see _Import & Export_). |
|
||||||
|
| About | The About & Help modal (keyboard shortcuts, about, and privacy information); privacy lives inside. |
|
||||||
|
| Donate | The Donate modal. The one text-labelled, soft-accent button — the solicitation keeps its word. |
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
|
- The utilities are icon-only so the header reads as quiet chrome (Carbon UI-shell header: global actions are a right-aligned icon row). The accessible names scope the workspace-level Import/Export ("Export workspace") apart from the preview pane's per-chart "Export" — the two were previously both labelled "Export" at once.
|
||||||
- Import and Export act directly (file dialog / file download); they do not open in-app modals.
|
- Import and Export act directly (file dialog / file download); they do not open in-app modals.
|
||||||
- The Datasets, About, and Donate entry points each open a modal (see _Modal System_).
|
- The Datasets, About, and Donate entry points each open a modal (see _Modal System_).
|
||||||
- **Settings are not a header entry point.** A design review (see _Settings_) distributed
|
- **Settings are not a header entry point.** A design review (see _Settings_) distributed
|
||||||
|
|||||||
+8
-59
@@ -8,65 +8,9 @@ record the resolution into the contract (`docs/architecture/09`+`10` and the rel
|
|||||||
|
|
||||||
## Open
|
## Open
|
||||||
|
|
||||||
- **Two visible "Export" buttons** (`App.tsx` header — workspace export;
|
_(empty — the 2026-06-13 batch resolved all eleven open items; resolutions live in
|
||||||
`ChartExport.tsx` in the preview header — per-chart export). Same label,
|
spec §01B/§02/§03D, arch 09 §5 (header worked example), and arch 10 §5 (toggle-strip
|
||||||
different scope, both visible at once. Candidate fix: rename one ("Export
|
pressed cue, SelectControl action row + trigger cap, no-affordance-for-unbuilt-features).)_
|
||||||
chart" / "Export workspace") or differentiate via grouping.
|
|
||||||
|
|
||||||
- **"LATER" facet placeholders in Chart Builder shelves**
|
|
||||||
(`ChartBuilderModal.module.css` → `.facetSlot`/`.facetTag`). Internal roadmap
|
|
||||||
language shipped to users. Either hide the affordance until faceting works or
|
|
||||||
write user-facing copy.
|
|
||||||
|
|
||||||
- **Default snippet names are timestamps** ("Snippet 2026-06-10 18:04:37") — the
|
|
||||||
library reads like a downloads folder next to builder-generated names ("Bar
|
|
||||||
chart of count by Ship Mode"). Candidate: derive a content-based name from the
|
|
||||||
spec (mark + encoded fields) when the user never renames.
|
|
||||||
|
|
||||||
- **Fit-mode tab labels** ("Original / Width / Height / Full", `LivePreview.tsx`)
|
|
||||||
are terse for a first-time user; tooltips likely suffice.
|
|
||||||
|
|
||||||
- **Donate is the most saturated element on screen** (header soft-accent wash) —
|
|
||||||
visually outranks Publish/Build Chart. Decide whether intentional.
|
|
||||||
|
|
||||||
- **Left rail affordance** (`PaneToggleStrip.tsx`): three unlabeled glyphs + a
|
|
||||||
tall empty strip; pane toggles aren't discoverable without hovering.
|
|
||||||
|
|
||||||
- **Pane toggle pressed state = hover state** (`PaneToggleStrip.module.css`).
|
|
||||||
The pressed chip is a `--layer-02` fill — the same fill the strip's IconButtons
|
|
||||||
use on hover — so hovering an off toggle looks identical to an on toggle at
|
|
||||||
rest. (The pressed chip's `--border-strong` border used to disambiguate; it was
|
|
||||||
dropped in the box-discipline pass.) Candidate cues: a filled/accent glyph,
|
|
||||||
an inset edge marker, or restoring a border for the on state only.
|
|
||||||
|
|
||||||
- **Dataset detail schema density** (`DatasetsModal.tsx`): one field per row
|
|
||||||
makes a 25-column dataset a long scroll before the preview table; a denser
|
|
||||||
two-column field list would fit overview + schema + preview on one screen.
|
|
||||||
|
|
||||||
- **Chart theme picker placement & header crowding** (`LivePreview.tsx` — ChartThemeControl).
|
|
||||||
The picker sits in the preview header because nesting a SelectControl inside the
|
|
||||||
PreviewSettings popover is impossible today (one-open-popover registry: the select would
|
|
||||||
close/unmount its own parent). Header now holds Fit + theme + export + gear; at narrow
|
|
||||||
pane widths the long trigger labels ("FiveThirtyEight", "Urban Institute") may crowd it.
|
|
||||||
Council questions: does the picker deserve header prominence (the "transform your chart"
|
|
||||||
showcase) or settings-cluster placement (a persistent global pref); should the popover
|
|
||||||
registry learn nesting; should the preset block also carry a group _heading_ (a
|
|
||||||
divider now bounds it — 2026-06-12, user-decided; `SelectControl` `dividerBefore`)?
|
|
||||||
|
|
||||||
- **"Edit themes…" action row inside the value picker** (`LivePreview.tsx` —
|
|
||||||
ChartThemeControl). A non-value action lives inside a single-select disclosure (the
|
|
||||||
VS Code theme-picker pattern), placed after the custom-themes block and before the
|
|
||||||
preset roster (first-use feedback: at the very bottom it was invisible without
|
|
||||||
scrolling). The ours-vs-presets divider (2026-06-12) now bounds it below; remaining
|
|
||||||
council questions: should the action row itself be styled distinctly from the value
|
|
||||||
rows; is a mid-list row that opens a modal instead of selecting surprising to AT
|
|
||||||
users?
|
|
||||||
|
|
||||||
- **Theme Builder config editor is a plain textarea** (`ThemeBuilderModal.tsx`). Monaco
|
|
||||||
(with the Vega-Lite config schema for completions) would match the main editor but is
|
|
||||||
heavy inside a modal and untested in that mounting. Revisit whether the builder deserves
|
|
||||||
a Monaco instance, and whether the gallery's canvas charts need text alternatives
|
|
||||||
beyond the per-card captions.
|
|
||||||
|
|
||||||
## Deferred (not design debts, revisit on demand)
|
## Deferred (not design debts, revisit on demand)
|
||||||
|
|
||||||
@@ -74,3 +18,8 @@ record the resolution into the contract (`docs/architecture/09`+`10` and the rel
|
|||||||
be a progressive enhancement on top of the chooser model, not a replacement. Revisit if
|
be a progressive enhancement on top of the chooser model, not a replacement. Revisit if
|
||||||
real usage asks for it (likely alongside Phase 4 faceting, where drag-to-shelf reads most
|
real usage asks for it (likely alongside Phase 4 faceting, where drag-to-shelf reads most
|
||||||
naturally).
|
naturally).
|
||||||
|
|
||||||
|
- **Theme Builder config editor stays a plain textarea** (decided 2026-06-13) — a second
|
||||||
|
Monaco mount is heavy inside a modal for an occasional surface; the parse error is the
|
||||||
|
feedback channel that matters. Revisit only if real usage asks for config completions.
|
||||||
|
The gallery's per-card captions stand as the canvas charts' text alternative.
|
||||||
|
|||||||
+19
-23
@@ -6,6 +6,7 @@ import { ModalShell } from './components/ModalShell';
|
|||||||
import { Onboarding } from './components/Onboarding';
|
import { Onboarding } from './components/Onboarding';
|
||||||
import { PaneSplitHandle } from './components/PaneSplitHandle';
|
import { PaneSplitHandle } from './components/PaneSplitHandle';
|
||||||
import { Icon } from './components/Icon';
|
import { Icon } from './components/Icon';
|
||||||
|
import { IconButton } from './components/IconButton';
|
||||||
import { PaneToggleStrip } from './components/PaneToggleStrip';
|
import { PaneToggleStrip } from './components/PaneToggleStrip';
|
||||||
import { ResizeHandle } from './components/ResizeHandle';
|
import { ResizeHandle } from './components/ResizeHandle';
|
||||||
import { SnippetLibrary } from './components/SnippetLibrary';
|
import { SnippetLibrary } from './components/SnippetLibrary';
|
||||||
@@ -77,46 +78,41 @@ export function App() {
|
|||||||
<span className={styles.version}>v{__APP_VERSION__}</span>
|
<span className={styles.version}>v{__APP_VERSION__}</span>
|
||||||
<span className={styles.spacer} />
|
<span className={styles.spacer} />
|
||||||
{/* Header actions establish a hierarchy (Carbon button/usage → one
|
{/* Header actions establish a hierarchy (Carbon button/usage → one
|
||||||
high-emphasis action per region; utilities in dashboards/toolbars read
|
high-emphasis action per region; Carbon UI-shell header → global
|
||||||
better at low emphasis). The utilities are ghost (borderless) buttons
|
actions are a right-aligned row of icon-only buttons). The utilities
|
||||||
with a leading icon for scanning; a divider then sets off Donate — the
|
are IconButtons — tooltips carry the full action, accessible names
|
||||||
one solicitation, given a soft-accent wash — and the theme toggle. */}
|
scope it ("Export workspace" vs the preview pane's per-chart
|
||||||
<Button
|
"Export") — then a divider sets off Donate — the one solicitation,
|
||||||
variant="ghost"
|
given a soft-accent wash — and the theme toggle. */}
|
||||||
|
<IconButton
|
||||||
|
label="Datasets"
|
||||||
onClick={() => openModal('datasets')}
|
onClick={() => openModal('datasets')}
|
||||||
aria-keyshortcuts="Meta+K Control+K"
|
aria-keyshortcuts="Meta+K Control+K"
|
||||||
title="Datasets (⌘/Ctrl+K)"
|
title="Datasets (⌘/Ctrl+K)"
|
||||||
>
|
>
|
||||||
<Icon name="dataset" />
|
<Icon name="dataset" />
|
||||||
Datasets
|
</IconButton>
|
||||||
</Button>
|
<IconButton
|
||||||
<Button
|
label="Import workspace"
|
||||||
variant="ghost"
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
title="Import a workspace JSON file"
|
title="Import a workspace JSON file"
|
||||||
>
|
>
|
||||||
<Icon name="import" />
|
<Icon name="import" />
|
||||||
Import
|
</IconButton>
|
||||||
</Button>
|
<IconButton
|
||||||
{/* TODO: two controls labelled "Export" are visible at once (this
|
label="Export workspace"
|
||||||
workspace export + the preview pane's per-chart export) — rename or
|
|
||||||
differentiate (docs/ux-second-pass.md). */}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => exportWorkspace()}
|
onClick={() => exportWorkspace()}
|
||||||
title="Export your workspace to a JSON file"
|
title="Export your workspace to a JSON file"
|
||||||
>
|
>
|
||||||
<Icon name="export" />
|
<Icon name="export" />
|
||||||
Export
|
</IconButton>
|
||||||
</Button>
|
<IconButton
|
||||||
<Button
|
label="About"
|
||||||
variant="ghost"
|
|
||||||
onClick={() => openModal('about')}
|
onClick={() => openModal('about')}
|
||||||
title="About, keyboard shortcuts, and privacy information"
|
title="About, keyboard shortcuts, and privacy information"
|
||||||
>
|
>
|
||||||
<Icon name="info" />
|
<Icon name="info" />
|
||||||
About
|
</IconButton>
|
||||||
</Button>
|
|
||||||
<span className={styles.headerDivider} aria-hidden="true" />
|
<span className={styles.headerDivider} aria-hidden="true" />
|
||||||
<Button variant="soft-accent" onClick={() => openModal('donate')} title="Support Astrolabe">
|
<Button variant="soft-accent" onClick={() => openModal('donate')} title="Support Astrolabe">
|
||||||
Donate
|
Donate
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ export function AboutModal() {
|
|||||||
yourself.
|
yourself.
|
||||||
</li>
|
</li>
|
||||||
<li>After the first load, the app works fully offline.</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>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1007,25 +1007,3 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
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,
|
* 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
|
* 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
|
* chart. A Swap X/Y action flips the two axes. The slot rows are shelf-shaped so
|
||||||
* X/Y action flips the two axes.
|
* 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() {
|
function OnChartShelves() {
|
||||||
const swapXY = useChartBuilderStore((s) => s.swapXY);
|
const swapXY = useChartBuilderStore((s) => s.swapXY);
|
||||||
@@ -653,14 +642,12 @@ function OnChartShelves() {
|
|||||||
<span className={styles.shelfName}>Columns</span>
|
<span className={styles.shelfName}>Columns</span>
|
||||||
<div className={styles.shelfSlots}>
|
<div className={styles.shelfSlots}>
|
||||||
<ChannelSlot channel="x" hint="Add a field for the X axis" />
|
<ChannelSlot channel="x" hint="Add a field for the X axis" />
|
||||||
<FacetSlot kind="column" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.shelfStrip}>
|
<div className={styles.shelfStrip}>
|
||||||
<span className={styles.shelfName}>Rows</span>
|
<span className={styles.shelfName}>Rows</span>
|
||||||
<div className={styles.shelfSlots}>
|
<div className={styles.shelfSlots}>
|
||||||
<ChannelSlot channel="y" hint="Add a field for the Y axis" />
|
<ChannelSlot channel="y" hint="Add a field for the Y axis" />
|
||||||
<FacetSlot kind="row" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -235,12 +235,17 @@
|
|||||||
color: var(--text);
|
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 {
|
.columns {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
columns: 220px 2;
|
||||||
flex-direction: column;
|
column-gap: 0;
|
||||||
|
column-rule: var(--border-width) solid var(--border);
|
||||||
border: var(--border-width) solid var(--border);
|
border: var(--border-width) solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,10 +255,11 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
}
|
break-inside: avoid;
|
||||||
|
/* Hairline between rows; the negative margin tucks each column's first border
|
||||||
.column + .column {
|
under the container border (a `+`-sibling border would double there). */
|
||||||
border-top: var(--border-width) solid var(--border);
|
border-top: var(--border-width) solid var(--border);
|
||||||
|
margin-top: calc(-1 * var(--border-width));
|
||||||
}
|
}
|
||||||
|
|
||||||
.columnName {
|
.columnName {
|
||||||
|
|||||||
@@ -41,10 +41,12 @@ import styles from './LivePreview.module.css';
|
|||||||
|
|
||||||
/** The four fit modes in display order (spec §04 → Fit / Sizing Modes). */
|
/** The four fit modes in display order (spec §04 → Fit / Sizing Modes). */
|
||||||
const FIT_OPTIONS: ReadonlyArray<SegmentedOption<FitMode>> = [
|
const FIT_OPTIONS: ReadonlyArray<SegmentedOption<FitMode>> = [
|
||||||
{ value: 'default', label: 'Original' },
|
// `title` doubles as the accessible name, so it leads with the visible label
|
||||||
{ value: 'width', label: 'Width' },
|
// (WCAG 2.5.3 label-in-name; see SegmentedOption.title).
|
||||||
{ value: 'height', label: 'Height' },
|
{ value: 'default', label: 'Original', title: 'Original — the natural size from the spec' },
|
||||||
{ value: 'full', label: 'Full' },
|
{ 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
|
* snippet's own `config` still overrides it property by property. Lives in the
|
||||||
* header, not inside PreviewSettings: SelectControl and SettingsPopover share
|
* header, not inside PreviewSettings: SelectControl and SettingsPopover share
|
||||||
* the one-open-popover registry, so a select nested in the popover would close
|
* 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. */
|
/** Sentinel option that opens the Theme Builder instead of selecting a theme. */
|
||||||
const EDIT_THEMES = 'edit-themes';
|
const EDIT_THEMES = 'edit-themes';
|
||||||
type ThemePickerValue = ChartThemeSelection | typeof 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
|
// 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
|
// chartThemeOptions where the order is decided, so the splice is the only
|
||||||
// index this component owns. The first preset is the divider-carrying option,
|
// index this component owns. The first preset is the divider-carrying option,
|
||||||
// so splicing right before it needs no count arithmetic.
|
// so splicing right before it needs no count arithmetic. The row needs no
|
||||||
// TODO: action row inside a value picker (visual separation? AT surprise?)
|
// special styling: SelectControl options are real buttons (not listbox
|
||||||
// parked for the batched council pass (docs/ux-second-pass.md).
|
// options), the "…" is the opens-further-UI convention, and `hasPopup`
|
||||||
|
// announces the dialog to AT.
|
||||||
const firstPreset = list.findIndex((o) => o.dividerBefore);
|
const firstPreset = list.findIndex((o) => o.dividerBefore);
|
||||||
list.splice(firstPreset === -1 ? list.length : firstPreset, 0, {
|
list.splice(firstPreset === -1 ? list.length : firstPreset, 0, {
|
||||||
value: EDIT_THEMES,
|
value: EDIT_THEMES,
|
||||||
label: 'Edit themes…',
|
label: 'Edit themes…',
|
||||||
detail: 'Create and manage custom themes',
|
detail: 'Create and manage custom themes',
|
||||||
|
hasPopup: 'dialog',
|
||||||
});
|
});
|
||||||
return list;
|
return list;
|
||||||
}, [customThemes]);
|
}, [customThemes]);
|
||||||
|
|||||||
@@ -17,14 +17,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* The controls are shared IconButtons; only the toggle-on state is local.
|
/* 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.) */
|
(.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 {
|
.strip .pressed {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: var(--layer-02);
|
background: var(--layer-02);
|
||||||
|
box-shadow: inset 3px 0 0 var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
|
|||||||
@@ -42,6 +42,10 @@
|
|||||||
|
|
||||||
.triggerLabel {
|
.triggerLabel {
|
||||||
min-width: 0;
|
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;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ export interface SelectControlOption<V extends string> {
|
|||||||
* the user's themes and the preset roster.
|
* the user's themes and the preset roster.
|
||||||
*/
|
*/
|
||||||
dividerBefore?: boolean;
|
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> {
|
export interface SelectControlProps<V extends string> {
|
||||||
@@ -162,6 +168,7 @@ export function SelectControl<V extends string>({
|
|||||||
type="button"
|
type="button"
|
||||||
className={`${styles.option} ${selected ? styles.selected : ''}`}
|
className={`${styles.option} ${selected ? styles.selected : ''}`}
|
||||||
aria-current={selected || undefined}
|
aria-current={selected || undefined}
|
||||||
|
aria-haspopup={o.hasPopup}
|
||||||
onClick={() => choose(o.value)}
|
onClick={() => choose(o.value)}
|
||||||
>
|
>
|
||||||
<span className={styles.optionLabel}>
|
<span className={styles.optionLabel}>
|
||||||
|
|||||||
@@ -233,9 +233,10 @@ export function ThemeBuilderModal() {
|
|||||||
<label className={styles.label} htmlFor="theme-config">
|
<label className={styles.label} htmlFor="theme-config">
|
||||||
Config (Vega-Lite JSON)
|
Config (Vega-Lite JSON)
|
||||||
</label>
|
</label>
|
||||||
{/* TODO: plain textarea vs a Monaco instance (config-schema completions), and
|
{/* A plain textarea, deliberately not Monaco: a second Monaco mount
|
||||||
text alternatives for the canvas gallery beyond the captions — parked for
|
is heavy inside a modal for an occasional surface, and the parse
|
||||||
the batched council pass (docs/ux-second-pass.md). */}
|
error below is the feedback channel that matters here. Revisit
|
||||||
|
only if real usage asks for config completions. */}
|
||||||
<textarea
|
<textarea
|
||||||
id="theme-config"
|
id="theme-config"
|
||||||
className={styles.configText}
|
className={styles.configText}
|
||||||
|
|||||||
Reference in New Issue
Block a user