mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Docs: move point-in-time records to exploration/, trim build-narration, lift open items to plan
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -30,12 +30,9 @@ A store is a module that calls `create<State>()` 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<SnippetState>((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 (
|
||||
<ul>
|
||||
{snippets.map((s) => (
|
||||
<li key={s.id} aria-current={s.id === activeSnippetId} onClick={() => select(s.id)}>
|
||||
{s.name}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
remove(s.id);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
// 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -422,7 +422,7 @@ export async function readOriginUsage(): Promise<number | undefined> {
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user