Fix documentation drift surfaced by the full-docs consistency review

This commit is contained in:
2026-06-09 16:51:43 +03:00
parent 0a652cf04b
commit 418bf23cd8
6 changed files with 96 additions and 82 deletions
+27 -32
View File
@@ -36,11 +36,12 @@ registry, coordinator, and shell are exhaustively type-checked.
// src/app/modals/types.ts
export type ModalName =
| 'datasets' // Datasets manager (list / detail / new-dataset form)
| 'settings' // Appearance, editor, performance, formatting prefs
| 'about' // About & Help (shortcuts, privacy)
| 'donate' // Donate
| 'chartBuilder' // Visual no-JSON chart composition for a dataset
| 'extract'; // Extract inline spec data into a new dataset
// Settings is NOT a modal — preferences are distributed to per-pane disclosure
// popovers (spec §01C/§07; see components/SettingsPopover).
export type ActiveModal = ModalName | null;
```
@@ -76,8 +77,8 @@ export interface ModalConfig {
init?: (arg?: string) => void;
/** Serializable snapshot of in-progress edits, used to detect unsaved
* changes on close. OMIT for modals that apply immediately (settings,
* about, donate) — omission opts out of the discard-confirmation. */
* changes on close. OMIT for modals with no in-progress edits to guard
* (about, donate) — omission opts out of the discard-confirmation. */
getState?: () => Record<string, unknown> | null;
/** Whether the modal's primary action (Save / Apply) should be blocked
@@ -88,16 +89,24 @@ export interface ModalConfig {
getError?: () => string | null;
/** Whether this modal is reflected in the URL hash (back/forward, reload
* restore). Datasets and Chart Builder are navigable; Donate is not. */
* restore). Datasets and Chart Builder are navigable; About/Donate/Extract
* are not (spec §01E). */
isUrlNavigable?: boolean;
}
```
> **Shipped divergence.** The implemented registry (`modals/modal-registry.ts`) **omits
> `hasError`/`getError`**: each modal renders its **own action row** inside its body (the
> multi-view Datasets manager doesn't fit a single shell-level Save/Cancel), so validity is
> each modal's own concern. The shipped `ModalConfig` keeps only `getState` (close-time
> unsaved-change detection) plus `init`/`isUrlNavigable`. The generic-footer sketch through
> the rest of this section is retained as the simpler pattern for a single-action modal —
> treat it as illustrative, not a description of current code.
### Example entries
```ts
import { DatasetsModal } from '../components/DatasetsModal';
import { SettingsModal } from '../components/SettingsModal';
import { ChartBuilderModal } from '../components/ChartBuilderModal';
import { ExtractModal } from '../components/ExtractModal';
import { DonateModal } from '../components/DonateModal';
@@ -105,7 +114,6 @@ import { AboutModal } from '../components/AboutModal';
import { useDatasetStore } from '../stores/DatasetStore';
import { useChartBuilderStore } from '../stores/ChartBuilderStore';
import { useExtractStore } from '../stores/ExtractStore';
import { useSettingsStore } from '../stores/SettingsStore';
// Per-modal transient state lives in the relevant feature store; the registry
// reads it via `getState()` (Zustand), never through component hooks.
@@ -152,22 +160,8 @@ export const MODAL_REGISTRY: Record<ModalName, ModalConfig> = {
getError: () => (useExtractStore.getState().name.trim() ? null : 'modals.extract.nameRequired'),
},
// Applies immediately — no getState, so closing never prompts.
settings: {
name: 'settings',
title: 'modals.settings.title',
component: SettingsModal,
isUrlNavigable: true,
init: () => useSettingsStore.getState().loadFromPrefs(),
},
// Pure info modals — no state, no validity, not navigable for donate.
about: {
name: 'about',
title: 'modals.about.title',
component: AboutModal,
isUrlNavigable: true,
},
// Pure info modals — no state, no validity, not navigable.
about: { name: 'about', title: 'modals.about.title', component: AboutModal },
donate: { name: 'donate', title: 'modals.donate.title', component: DonateModal },
};
```
@@ -297,9 +291,9 @@ export function hasUnsavedChanges(): boolean {
```
The snapshot is taken once on open and compared on close. Modals without
`getState` (settings, about, donate) snapshot to `null`, so `hasUnsavedChanges`
short-circuits and they close instantly — correct, because they either apply
immediately or hold nothing to lose.
`getState` (about, donate) snapshot to `null`, so `hasUnsavedChanges`
short-circuits and they close instantly — correct, because they hold nothing to
lose.
### Validity passthrough
@@ -512,8 +506,8 @@ entry. Three properties force the split:
component; its title/message/labels are supplied at the call site. There's
nothing to register.
- **Stacks _above_ a feature modal.** The discard-changes prompt must appear over
an already-open Datasets/Settings modal — which directly violates the feature
layer's "at most one open" rule. So confirmations live on a higher z-layer
an already-open feature modal (e.g. Datasets or Chart Builder) — which directly
violates the feature layer's "at most one open" rule. So confirmations live on a higher z-layer
(`z-index: 1000`, above the future modal shell).
- **Not navigable.** A confirmation is never a URL destination or a reload-restore
target; it only exists for the duration of one decision.
@@ -579,15 +573,16 @@ tool.
The coordinator is the join point for navigation:
- `openModal` calls `syncModalToUrl`; navigable modals write a hash
(`#datasets`, `#datasets/dataset-<id>`, `#datasets/dataset-<id>/build`,
`#settings`). Non-navigable modals (donate) write nothing.
(`#datasets`, `#datasets/dataset-<id>`, `#datasets/dataset-<id>/build`).
Non-navigable modals (about, donate, extract) write nothing.
- `closeModal` calls `clearModalFromUrl`, returning to the underlying workspace
hash.
- On load, the URL restorer reads the hash and calls `openModal(name, arg)` to
rehydrate the right modal and sub-target.
- The global key handler maps `Cmd/Ctrl+K``toggleDatasets()`,
`Cmd/Ctrl+,``openModal('settings')`, and `Escape` `closeModal()` (the
Escape binding is a no-op when `activeModal` is `null`).
- The global key handler maps `Cmd/Ctrl+K``toggleDatasets()` and `Escape`
`closeModal()` (a no-op when `activeModal` is `null`). `Cmd/Ctrl+,` opens the
editor **settings popover**, not a modal (`openSettingsPopover('editor-settings')`;
settings are distributed — spec §01C/§07).
Because all of these call the same coordinator functions, browser
Back/Forward, keyboard shortcuts, and in-app triggers stay consistent — they