diff --git a/AGENTS.md b/AGENTS.md index 334992a..d4673d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,7 @@ docs/ ├── spec/ # Authoritative behavioral specification (00–10) — the WHAT ├── architecture/ # Architecture playbook (00–10) — the HOW (self-contained) │ └── visual-specimen.html # Standalone token sandbox + reusable-primitive catalog +├── exploration/ # Point-in-time records (research, reviews, scope memos) — not maintained ├── IMPLEMENTATION-PLAN.md # Milestone sequence (M0–M6) └── WHY-A-SEPARATE-REBUILD.md ``` diff --git a/docs/IMPLEMENTATION-PLAN.md b/docs/IMPLEMENTATION-PLAN.md index 5e4088a..fce0dba 100644 --- a/docs/IMPLEMENTATION-PLAN.md +++ b/docs/IMPLEMENTATION-PLAN.md @@ -65,7 +65,7 @@ Vite + React + Zustand + TypeScript + Vitest (happy-dom) + vite-plugin-pwa. `src/core` ↔ `src/app` split, `useAppStore`, design tokens, three-pane placeholder shell, first core module (`format-detection`) with tests. -**Verified:** `npm run typecheck`, `npm test` (14 passing), `npm run build` (PWA SW generated). +**Verified:** `npm run typecheck`, `npm test`, `npm run build` (PWA SW generated). --- @@ -167,7 +167,7 @@ and the companion `visual-specimen.html`. - Keyboard focus ring visible; text/UI contrast passes AA in light and dark. - No placeholder styling remains on the M1 surfaces. -**Verified:** `typecheck` + `test` (61 passing, incl. `vega-themes.test.ts`) + +**Verified:** `typecheck` + `test` (incl. `vega-themes.test.ts`) + `build` (Plex woff2, all script subsets, bundled & precached via the PWA `globPatterns`). Both themes screenshotted via the real app (chrome + Monaco + chart all repaint on theme flip); focus ring visible. @@ -216,7 +216,7 @@ assistance, and the fit-mode rendering contract. - Invalid spec shows inline error; autocomplete suggests Vega-Lite properties. - Each fit mode resizes the chart as specified; choice survives reload. -**Verified:** `typecheck` + `test` (83 passing — `rendering` fit-mode incl. +**Verified:** `typecheck` + `test` (`rendering` fit-mode incl. nested layer/concat/facet specs, `SnippetStore` draft/publish/revert/editorView, `settings-store` `previewFitMode` round-trip) + `build` (PWA, 41 precache entries) + `eslint` clean. Implementation notes: editing now writes the @@ -290,9 +290,9 @@ the reference. **Goal:** no-JSON chart composition from a dataset → a new snippet. > **Enhancement push (post-M4).** The forward plan now lives in -> [`docs/chart-builder-enhancement-scope.md`](chart-builder-enhancement-scope.md) — it merges -> the Tier-B backlog ([`chart-builder-research.md`](chart-builder-research.md) §8) with the -> Lyra interaction review ([`lyra-review.md`](lyra-review.md)) and sets a **Tier-C** target. +> [`docs/exploration/chart-builder-enhancement-scope.md`](exploration/chart-builder-enhancement-scope.md) — it merges +> the Tier-B backlog ([`chart-builder-research.md`](exploration/chart-builder-research.md) §8) with the +> Lyra interaction review ([`lyra-review.md`](exploration/lyra-review.md)) and sets a **Tier-C** target. > Shipped beyond the Tier-B floor so far: per-channel aggregate/bin/`timeUnit`, sort/stack; > **actionable hints** (one-click warning fixes); and a builder UX/perf batch (near-fullscreen > modal, canvas preview + canvas max-dimension guard, data-aware default pre-population). See @@ -326,7 +326,7 @@ canon): this is where Astrolabe stops being a pass-through JSON editor and start chart-shaped suggestions/defaults, so "_which chart, and why_" becomes a decision the app owns — the one thing Carbon's data-viz styling doesn't cover. Rather than a styling-only seating, we ran a full **research-first** pass (FT + Datawrapper + the formal engines -**Draco** and **Voyager**), recorded in [`docs/chart-builder-research.md`](chart-builder-research.md), +**Draco** and **Voyager**), recorded in [`docs/exploration/chart-builder-research.md`](exploration/chart-builder-research.md), and chose the **Tier B "smart + guarded"** design: smart default mark for the data shape, valid-type-locked field-type menus, Size-channel discipline, and non-blocking guidance. The convergent rules and citations live in that doc; the spec (§06) was amended to match. @@ -372,7 +372,7 @@ debounces only keystroke (`shownText`-only) changes. **Deferred at the time to M5/M6 (per §02):** Search, Sort controls + persistence, two distinct empty-state messages, Storage Monitor — all now delivered in M6. -**Verified:** `typecheck` + `test` (287 passing — `snippet` duplicate factory, +**Verified:** `typecheck` + `test` (`snippet` duplicate factory, `SnippetStore` rename/comment/duplicate, a `SnippetLibrary` render test guarding the auto-save effect against a render loop) + `eslint` clean + `build` (PWA, 41 precache entries). @@ -507,6 +507,28 @@ not a silent drift: --- +## Open items (carried from the exploration memos) + +Deferred features whose reasoning lives in `docs/exploration/`; pulled here so the backlog +is in the maintained plan, not the archive. + +**Chart theming** (`exploration/chart-theming-scope.md`): + +- **User font upload** — FontFace-from-IndexedDB tier; theme `fonts: { family, source: 'file' }`. +- **Google Fonts opt-in CDN tier** — keyless catalog, opt-in only. +- **SVG export font embedding** — embed face data so exported SVGs render off-app. +- **Theme↔font pairing metadata** — a suggestion nicety. +- **Built-in expressive theme preset gallery** — e.g. "Editorial", "Terminal", "Sketch". + +**Chart Builder** (`exploration/chart-builder-enhancement-scope.md`): + +- **Transform-aware data inspector** — show resolved post-transform rows + (`exploration/data-inspector-exploration.md`). +- **Field-chip drag-and-drop** — click/keyboard-first shipped; drag deferred. +- **Calculated-field autocomplete popup** — Monaco-style completion for expressions. + +--- + ## Architecture reference The **how** behind each milestone is documented self-containedly in diff --git a/docs/architecture/00-overview.md b/docs/architecture/00-overview.md index 30d33e9..88e517b 100644 --- a/docs/architecture/00-overview.md +++ b/docs/architecture/00-overview.md @@ -22,11 +22,8 @@ about user-facing widgets. At that overlap, one rule keeps them from drifting: and never contradict it.** A playbook bullet may _name_ the behavior in one clause and cite the spec, then spend its words on the _how_ (the role, the keys, the focus move) and the _why_ (the council/canon citation). When a playbook bullet and the spec disagree, the - **spec wins** and the bullet is the bug. - -_(This bit us once: arch §10 described "keep the library list, title-only" while spec §02 -said "replace the entire list with the onboarding canvas" — the two halves were authored from -opposite mental models. The restatement, not the separation, was the leak.)_ + **spec wins** and the bullet is the bug. Restatement is the leak: two docs describing the + same behavior in their own words drift into contradiction; one cites the other instead. ## How to use this playbook diff --git a/docs/architecture/01-state-and-stores.md b/docs/architecture/01-state-and-stores.md index 4e1d8be..a0b4269 100644 --- a/docs/architecture/01-state-and-stores.md +++ b/docs/architecture/01-state-and-stores.md @@ -30,12 +30,9 @@ A store is a module that calls `create()` once and exports the resulting hook. The state object holds both **data fields** and **action functions**. ```ts -// src/app/stores/AppStore.ts import { create } from 'zustand'; -import type { UiTheme } from '@core/theme'; // defined in core; charts key off it too - -export type ModalName = 'datasets' | 'about' | 'donate' | 'chartBuilder' | 'extract'; +// ModalName is the union of the app's modal identifiers; UiTheme is defined in core. export interface AppState { uiTheme: UiTheme; activeModal: ModalName | null; @@ -297,36 +294,17 @@ export const useSnippetStore = create((set, get) => ({ })); ``` -The component is thin — it selects state and calls actions: +The component is thin — it selects state narrowly and wires events to actions, with no +mutation logic of its own: ```tsx -import { useShallow } from 'zustand/react/shallow'; -import { useSnippetStore } from '../stores/SnippetStore'; - export function SnippetList() { const { snippets, activeSnippetId } = useSnippetStore( useShallow((s) => ({ snippets: s.snippets, activeSnippetId: s.activeSnippetId })), ); - const select = useSnippetStore((s) => s.select); + const select = useSnippetStore((s) => s.select); // stable identity — select actions individually const remove = useSnippetStore((s) => s.remove); - - return ( - - ); + // render: one row per snippet, each calling select(id) / remove(id) on the events. } ``` @@ -508,11 +486,8 @@ couples every intermediate component to data it doesn't use. Each store exposes a `reset()` action that returns its fields to initial values (used on "new workspace", sign-out, or test teardown). Because every fact is a single source field with no hand-maintained duplicates, reset is a flat `set(...)` -of the initial values; selector-derived values recompute on their own. - -```ts -reset: () => set({ snippets: [], activeSnippetId: null, draftSpec: '' }); -``` +of the initial values (as in `SnippetStore` above); selector-derived values +recompute on their own. --- diff --git a/docs/architecture/02-persistence.md b/docs/architecture/02-persistence.md index 02ea39a..39400b9 100644 --- a/docs/architecture/02-persistence.md +++ b/docs/architecture/02-persistence.md @@ -422,7 +422,7 @@ export async function readOriginUsage(): Promise { } ``` -**The bar is decorative; the legend is the data.** There is no `role="meter"` — a meter needs a meaningful maximum, which a composition with no fixed ceiling lacks (APG → meter). The legend's text labels + sizes are the accessible source of truth, so meaning never rests on hue (WCAG 1.4.1). Dropping the old 0.8/0.95 "almost full" thresholds is deliberate: they keyed off the untrustworthy quota; the genuine out-of-room event surfaces at save time (below), where it is accurate. (Resolves the former spec §02 ↔ code "whole-origin vs. snippet budget" divergence; council resolution recorded in `docs/architecture/10`.) +**The bar is decorative; the legend is the data.** There is no `role="meter"` — a meter needs a meaningful maximum, which a composition with no fixed ceiling lacks (APG → meter). The legend's text labels + sizes are the accessible source of truth, so meaning never rests on hue (WCAG 1.4.1). There are no "almost full" percentage thresholds — a fraction would key off the untrustworthy quota; the genuine out-of-room event surfaces at save time (below), where it is accurate. ### Fail loudly, never silently lose data diff --git a/docs/architecture/05-rendering-theming-preview.md b/docs/architecture/05-rendering-theming-preview.md index 2e69198..70f134d 100644 --- a/docs/architecture/05-rendering-theming-preview.md +++ b/docs/architecture/05-rendering-theming-preview.md @@ -45,7 +45,7 @@ above it is data; everything below it is a Vega `View` we own and must tear down > the main Live Preview) reads runtime rows via `view.data(name)`. It rides this same > boundary: a `RenderHandle.data()` accessor wraps the view (like `toImageURL`), so no > component touches the raw view. Parked, not built — see -> [`docs/data-inspector-exploration.md`](../data-inspector-exploration.md). +> [`docs/exploration/data-inspector-exploration.md`](../exploration/data-inspector-exploration.md). --- @@ -126,8 +126,7 @@ async function rerender(node: HTMLElement, spec: TopLevelSpec, config: Config) { crisp/inspectable/copyable and stays the default for the editor's LivePreview. But an SVG chart renders one DOM node per mark, so a many-mark chart (e.g. the Chart Builder's default one-bar-per-row on a 10k-row dataset) costs **seconds** of main-thread - layout/paint per render (measured ~6.5s on 9994 rows; the chart paints _after_ it first - appears, freezing the tab). The **Chart Builder preview** therefore passes + layout/paint per render (the chart paints _after_ it first appears, freezing the tab). The **Chart Builder preview** therefore passes `renderSpec(…, { renderer: 'canvas' })` — canvas is a single node and paints in milliseconds. The raster trade-off is invisible for an ephemeral preview, and image export (`view.toImageURL`) is renderer-agnostic. @@ -165,7 +164,7 @@ background, the categorical color range. Astrolabe ships one config per UI theme so charts visually belong to the app rather than looking like stock Vega-Lite. `src/core/vega-themes.ts` is the single source of truth; each house config is **two merged layers** (the full audit and forward plan live in -[`docs/chart-theming-scope.md`](../chart-theming-scope.md)): +[`docs/exploration/chart-theming-scope.md`](../exploration/chart-theming-scope.md)): - **Base** (`lightBaseConfig`/`darkBaseConfig`) — the legibility minimum: `background: 'transparent'` (the pane shows through) plus guide colors on the diff --git a/docs/architecture/10-interaction-and-feedback.md b/docs/architecture/10-interaction-and-feedback.md index fa150ec..045226a 100644 --- a/docs/architecture/10-interaction-and-feedback.md +++ b/docs/architecture/10-interaction-and-feedback.md @@ -254,12 +254,9 @@ the Draft/Published view) is a **radio group**, never a row of `aria-pressed` to - `role="radio"`/`aria-checked`, a **roving tabindex** (only the selected option is a tab stop), and Arrow/Home/End to move-and-select (APG → Radio Group). One widget so the keyboard - model is defined once. _(Tabs were a candidate for Draft/Published; we chose radio group for - consistency with the other segmented controls and to avoid tabpanel wiring to Monaco. 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 - 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.)_ A per-option + model is defined once. _(Not a toggle switch: APG defines `role="switch"` as on/off of a **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 on/off settings.)_ 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). @@ -302,9 +299,9 @@ show real measured sizes instead. - **Unavailable degrades, not disappears.** Snippets + datasets are measured from our own data, so they always show; only the **App** segment (which needs the origin estimate) drops out when the Storage Manager API is absent. -- **No proactive "almost full" warning.** Dropping the old 0.8/0.95 thresholds is intentional — - they keyed off the untrustworthy quota, and a fake fuel gauge fails NN/g #1 (_visibility of system - status_) more than it serves it. The genuine out-of-room event surfaces at **save time** as an +- **No proactive "almost full" warning.** A percentage gauge would key off the untrustworthy + quota, and a fake fuel gauge fails NN/g #1 (_visibility of system status_) more than it + serves it. The genuine out-of-room event surfaces at **save time** as an actionable error (`services/storage-errors.ts` → recover by deleting), satisfying NN/g #9. _(Consulted via /council → WAI-ARIA APG `meter`, FT Visual Vocabulary + Datawrapper (part-to-whole), @@ -356,7 +353,7 @@ generous preview. Each card **renders live through the shared `chart-renderer`** embed path; each card owns its `RenderHandle` and finalizes on unmount — the per-card nodes are independent, so they don't touch `LivePreview`'s single-host serialization), and adds as an ordinary snippet. Empty stays calm and positive, never an error (NN/g aesthetic-and-minimalist; -§3 empty ≠ error). Council resolutions for the canvas: (1) **each card preview is decorative** — +§3 empty ≠ error). Three rules govern the canvas. (1) **Each card preview is decorative** — `aria-hidden`, skipped by screen readers (Carbon empty-states a11y / WCAG decorative images); the card **name + one-line description + a uniquely-labelled `Add` button** ("Add Bar chart", APG button) carry the meaning, so AT users reach no dead end. (2) The canvas is the **single @@ -386,7 +383,7 @@ primary next step ("Add a dataset" → the Datasets create form) — never a dea dataset-row "Build Chart" stays as the contextual shortcut; the onboarding canvas gains the data-first door ("Build a chart from your data") beside its primary. _(Consulted via /council → NN/g #6/#7, Carbon empty-states; recorded in -docs/chart-builder-enhancement-scope.md §3 · 3D. This bullet is the contract; cite it, not +docs/exploration/chart-builder-enhancement-scope.md §3 · 3D. This bullet is the contract; cite it, not the source.)_ **Resolved — one live region per shared message.** When the same error feeds two surfaces diff --git a/docs/exploration/README.md b/docs/exploration/README.md new file mode 100644 index 0000000..4b6601b --- /dev/null +++ b/docs/exploration/README.md @@ -0,0 +1,18 @@ +# docs/exploration + +Point-in-time records: research, tool reviews, and scope/planning memos written to think a +decision through. Unlike `docs/spec/` (the contract — the _what_) and `docs/architecture/` +(the maintained _how_), nothing here is kept current — each file reflects what was known +when it was written. Decisions that survived are folded into the spec and architecture; +these remain as the reasoning behind them. + +Active follow-up work is tracked in [`../IMPLEMENTATION-PLAN.md`](../IMPLEMENTATION-PLAN.md), +not here. Exact figures in these files (timings, line counts, test counts) are frozen +snapshots, not live numbers. + +- `chart-builder-research.md` — cross-source chart-choice research (Draco, Voyager, FT Visual Vocabulary, Datawrapper). +- `lyra-review.md` — review of vega/lyra for Chart Builder interaction ideas. +- `chart-builder-enhancement-scope.md` — consolidated Tier-B → Tier-C forward plan for the Chart Builder. +- `chart-theming-scope.md` — chart theming plan and slice breakdown. +- `monetization-and-sync-exploration.md` — monetization + BYO-cloud-sync direction memo. +- `data-inspector-exploration.md` — transform-aware data-inspector direction. diff --git a/docs/chart-builder-enhancement-scope.md b/docs/exploration/chart-builder-enhancement-scope.md similarity index 99% rename from docs/chart-builder-enhancement-scope.md rename to docs/exploration/chart-builder-enhancement-scope.md index 72eaa8b..015e7bc 100644 --- a/docs/chart-builder-enhancement-scope.md +++ b/docs/exploration/chart-builder-enhancement-scope.md @@ -4,7 +4,7 @@ > for chart-builder enhancement work — it merges the research backlog from > [`chart-builder-research.md`](./chart-builder-research.md) §8 (the M4 decision and its > deferred items) with the interaction ideas from [`lyra-review.md`](./lyra-review.md) §5, -> read against the current spec ([`spec/06-chart-builder.md`](./spec/06-chart-builder.md)) +> read against the current spec ([`spec/06-chart-builder.md`](../spec/06-chart-builder.md)) > and the shipped code (`src/core/chart-builder.ts`). > > **Goal (the brief):** a **rapid, intuitive GUI for building Vega-Lite specs**, with diff --git a/docs/chart-builder-research.md b/docs/exploration/chart-builder-research.md similarity index 100% rename from docs/chart-builder-research.md rename to docs/exploration/chart-builder-research.md diff --git a/docs/chart-theming-scope.md b/docs/exploration/chart-theming-scope.md similarity index 100% rename from docs/chart-theming-scope.md rename to docs/exploration/chart-theming-scope.md diff --git a/docs/data-inspector-exploration.md b/docs/exploration/data-inspector-exploration.md similarity index 97% rename from docs/data-inspector-exploration.md rename to docs/exploration/data-inspector-exploration.md index bcf104d..a35da4b 100644 --- a/docs/data-inspector-exploration.md +++ b/docs/exploration/data-inspector-exploration.md @@ -58,7 +58,7 @@ mapping those to friendly labels and choosing which to expose is the main design - Seed: builder **1D** data preview — see [`chart-builder-enhancement-scope.md`](./chart-builder-enhancement-scope.md) (§3 1D). -- Renderer/preview home: [`architecture/05`](./architecture/05-rendering-theming-preview.md) +- Renderer/preview home: [`architecture/05`](../architecture/05-rendering-theming-preview.md) (the `RenderHandle` boundary the `view.data` accessor extends). - Behaviour, once built, is specified in `docs/spec/04` (Live Preview) and `docs/spec/06` (the builder preview), per "spec follows code". diff --git a/docs/lyra-review.md b/docs/exploration/lyra-review.md similarity index 100% rename from docs/lyra-review.md rename to docs/exploration/lyra-review.md diff --git a/docs/monetization-and-sync-exploration.md b/docs/exploration/monetization-and-sync-exploration.md similarity index 99% rename from docs/monetization-and-sync-exploration.md rename to docs/exploration/monetization-and-sync-exploration.md index f9e9da4..9e57699 100644 --- a/docs/monetization-and-sync-exploration.md +++ b/docs/exploration/monetization-and-sync-exploration.md @@ -19,7 +19,7 @@ ## 1. The tension this collides with -A login + encrypted-DB system isn't a neutral feature add. [`SOUL.md`](../SOUL.md) makes +A login + encrypted-DB system isn't a neutral feature add. [`SOUL.md`](../../SOUL.md) makes "no account" a **named value**, and lists the opposite under _What We're Not_: - Value #5, _Own Your Data_: "Your library is a file you control, **not a row in someone's diff --git a/docs/spec/06-chart-builder.md b/docs/spec/06-chart-builder.md index 26d88f6..f38ac8c 100644 --- a/docs/spec/06-chart-builder.md +++ b/docs/spec/06-chart-builder.md @@ -2,7 +2,7 @@ The Chart Builder is a visual, no-JSON way to compose a Vega-Lite chart from a selected dataset. The user picks a mark type and maps the dataset's columns to encoding channels; the builder produces a complete Vega-Lite spec and saves it as a new snippet that references the dataset. It is intended for users who want to start a chart quickly without hand-writing JSON in the _Spec Editor & Draft/Published Workflow_. -> **Design level — "smart + guarded" (Tier B).** The builder is field-first — the user works from a shelf of the dataset's columns and drops them onto encoding channels — and stays within the inputs below, but it is not a dumb composer: it picks a sensible default mark for the data shape, offers only field types valid for each column, keeps unsuitable channel mappings out of reach, and surfaces non-blocking guidance for encodings that render poorly. These behaviors are derived from cross-source chart-choice research recorded in [`docs/chart-builder-research.md`](../chart-builder-research.md) (the convergence of Draco, Voyager, the FT Visual Vocabulary, and Datawrapper). On top of this "smart + guarded" base sits the **intent-first front door** (Tier C — _what do you want to show?_, see _Intent_ below): an on-ramp that recommends a whole chart from the user's stated intent, without replacing the mark-first builder beneath it. +> **Design level — "smart + guarded" (Tier B).** The builder is field-first — the user works from a shelf of the dataset's columns and drops them onto encoding channels — and stays within the inputs below, but it is not a dumb composer: it picks a sensible default mark for the data shape, offers only field types valid for each column, keeps unsuitable channel mappings out of reach, and surfaces non-blocking guidance for encodings that render poorly. These behaviors are derived from cross-source chart-choice research recorded in [`docs/exploration/chart-builder-research.md`](../exploration/chart-builder-research.md) (the convergence of Draco, Voyager, the FT Visual Vocabulary, and Datawrapper). On top of this "smart + guarded" base sits the **intent-first front door** (Tier C — _what do you want to show?_, see _Intent_ below): an on-ramp that recommends a whole chart from the user's stated intent, without replacing the mark-first builder beneath it. ## Opening @@ -52,7 +52,7 @@ The section is ordered **input → shaping** so the distinction reads at a glanc ### Data preview - The first item in the section: a collapsible, **read-only** sample of the dataset's first rows (capped), with a per-column **type chip** in each header. It lets the user sanity-check inferred types against the actual values _before_ building — exactly when type inference is most likely to surprise. Editing the data is out of scope. A non-tabular payload (a single JSON object, TopoJSON, an unfetched URL) has no rows to show. -- The preview shows the dataset's **raw source rows** — _before_ the filters and calculated fields below are applied; it does not show derived columns. Its position above those controls makes that explicit: it is the input, not the result. (The transformed result is what the right-hand chart renders. A transform-aware "data inspector" that shows the resolved rows is a planned future direction — see [`docs/data-inspector-exploration.md`](../data-inspector-exploration.md).) +- The preview shows the dataset's **raw source rows** — _before_ the filters and calculated fields below are applied; it does not show derived columns. Its position above those controls makes that explicit: it is the input, not the result. (The transformed result is what the right-hand chart renders. A transform-aware "data inspector" that shows the resolved rows is a planned future direction — see [`docs/exploration/data-inspector-exploration.md`](../exploration/data-inspector-exploration.md).) ### Filters @@ -122,7 +122,7 @@ These controls appear only when they apply: ### Guidance (non-blocking) -The builder surfaces short, plain-language hints for configurations that render but read poorly — advisory only, never blocking the **Create Snippet** action (validation below is the sole gate). A hint states the _problem_; where there is an obvious remedy, it also offers one or more **one-click fix** buttons that apply the change to the configuration (e.g. _Aggregate as Sum_, _Swap X/Y_, _Switch to Point_, _Stack_, _Remove colour_). A fix is an offer, never a forced change — applying it updates the config and the hint re-derives away. Interaction/accessibility of these actions follows [`architecture/10`](../architecture/10-interaction-and-feedback.md) §5 (polite announcement, focus moved off the removed button). These follow the chart-choice research ([`docs/chart-builder-research.md`](../chart-builder-research.md)) and include, for example: +The builder surfaces short, plain-language hints for configurations that render but read poorly — advisory only, never blocking the **Create Snippet** action (validation below is the sole gate). A hint states the _problem_; where there is an obvious remedy, it also offers one or more **one-click fix** buttons that apply the change to the configuration (e.g. _Aggregate as Sum_, _Swap X/Y_, _Switch to Point_, _Stack_, _Remove colour_). A fix is an offer, never a forced change — applying it updates the config and the hint re-derives away. Interaction/accessibility of these actions follows [`architecture/10`](../architecture/10-interaction-and-feedback.md) §5 (polite announcement, focus moved off the removed button). These follow the chart-choice research ([`docs/exploration/chart-builder-research.md`](../exploration/chart-builder-research.md)) and include, for example: - A **Line**, **Area**, or **Heatmap** mark with only one axis mapped (both axes are needed to draw it). - A **Bar/Line/Area** whose X and Y are both categories (nothing to measure). diff --git a/docs/spec/09-data-model.md b/docs/spec/09-data-model.md index 8f77b69..8ebbea3 100644 --- a/docs/spec/09-data-model.md +++ b/docs/spec/09-data-model.md @@ -126,4 +126,4 @@ A **CustomTheme** is a user-named Vega-Lite config saved in the library and offe | `created` | ISO-timestamp string | When the theme was first created. | | `modified` | ISO-timestamp string | When the theme was last changed. | -Selection is keyed by `id` (not name) so renaming a theme never invalidates the persisted `ui.chartTheme`. A persisted `custom:` whose record no longer exists is not an error: charts render with the house style until the record appears (themes hydrate asynchronously), and deleting the actively-selected theme resets the selection to `astrolabe` explicitly. Custom themes are not yet included in the _Import & Export_ envelope (planned; see `docs/chart-theming-scope.md`). +Selection is keyed by `id` (not name) so renaming a theme never invalidates the persisted `ui.chartTheme`. A persisted `custom:` whose record no longer exists is not an error: charts render with the house style until the record appears (themes hydrate asynchronously), and deleting the actively-selected theme resets the selection to `astrolabe` explicitly. Custom themes travel in the _Import & Export_ envelope alongside snippets and datasets (spec §08).