mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
113 lines
4.7 KiB
TypeScript
113 lines
4.7 KiB
TypeScript
/**
|
||
* Modal registry (docs/architecture/03 → Layer 1).
|
||
*
|
||
* One metadata entry per feature modal — the single source of truth the
|
||
* coordinator and shell read, so adding a modal is one entry plus its component
|
||
* rather than edits scattered across the shell, URL sync, and close logic.
|
||
*
|
||
* Divergence from the arch sketch's optional generic footer: each modal renders
|
||
* its OWN action row inside its body (the Datasets manager is multi-view, so a
|
||
* single shell-level Save/Cancel doesn't fit). The registry therefore omits
|
||
* `hasError`/`getError` (validity is the modal's own concern) and keeps only
|
||
* `getState` for unsaved-change detection on close. The registry is a partial
|
||
* map — only the implemented ones are registered here. Settings is deliberately NOT
|
||
* a modal — preferences are distributed to per-pane disclosure popovers (spec §07;
|
||
* see components/SettingsPopover).
|
||
*/
|
||
|
||
import type { ComponentType } from 'react';
|
||
import type { ActiveModal, ModalName } from './types';
|
||
import { AboutModal } from '../components/AboutModal';
|
||
import { ChartBuilderModal } from '../components/ChartBuilderModal';
|
||
import { DatasetsModal } from '../components/DatasetsModal';
|
||
import { DonateModal } from '../components/DonateModal';
|
||
import { ExtractModal } from '../components/ExtractModal';
|
||
import { useChartBuilderStore } from '../stores/ChartBuilderStore';
|
||
import { useDatasetStore } from '../stores/DatasetStore';
|
||
import { useExtractStore } from '../stores/ExtractStore';
|
||
|
||
export interface ModalConfig {
|
||
name: ModalName;
|
||
/** Header title (literal for now; an i18n key once strings are centralized). */
|
||
title: string;
|
||
/** The body rendered inside the shell. */
|
||
component: ComponentType;
|
||
/** Initialize transient state on open. `arg` carries an optional sub-target. */
|
||
init?: (arg?: string) => void;
|
||
/**
|
||
* Serializable snapshot of in-progress edits for unsaved-change detection.
|
||
* Return null when there is nothing to lose (browsing, or no open form) so
|
||
* closing doesn't prompt. Omit entirely for modals that apply immediately.
|
||
*/
|
||
getState?: () => Record<string, unknown> | null;
|
||
/** Whether the modal is reflected in the URL hash (navigable). */
|
||
isUrlNavigable?: boolean;
|
||
/**
|
||
* Whether a backdrop (click-outside) closes the modal. Defaults to `true`. Set
|
||
* `false` for modals holding in-progress work an accidental click shouldn't
|
||
* discard (the Chart Builder) — Escape and the close button still dismiss.
|
||
*/
|
||
dismissOnBackdrop?: boolean;
|
||
}
|
||
|
||
export const MODAL_REGISTRY: Partial<Record<ModalName, ModalConfig>> = {
|
||
// Navigable, multi-view manager. The snapshot captures only the open create/edit
|
||
// form, so browsing list↔detail never trips a false discard prompt.
|
||
datasets: {
|
||
name: 'datasets',
|
||
title: 'Datasets',
|
||
component: DatasetsModal,
|
||
isUrlNavigable: true,
|
||
init: (datasetId) => useDatasetStore.getState().select(datasetId ? Number(datasetId) : null),
|
||
getState: () => {
|
||
const s = useDatasetStore.getState();
|
||
return s.view === 'new' || s.view === 'edit' ? { form: s.form } : null;
|
||
},
|
||
},
|
||
|
||
// Opened from the snippet editor with the active draft's inline data to lift out.
|
||
extract: {
|
||
name: 'extract',
|
||
title: 'Extract to Dataset',
|
||
component: ExtractModal,
|
||
init: () => useExtractStore.getState().init(),
|
||
getState: () => ({ name: useExtractStore.getState().name }),
|
||
},
|
||
|
||
// Opened from a selected dataset's "Build Chart" action; `arg` is its id. Loads
|
||
// the dataset and pre-populates a smart default config (§06). Applies on Create
|
||
// (a new snippet), so there is nothing transient to lose on close — no getState.
|
||
// Backdrop dismissal is off: the config is real in-progress work, and a stray
|
||
// click outside this large surface shouldn't throw it away (Escape/× still close).
|
||
chartBuilder: {
|
||
name: 'chartBuilder',
|
||
title: 'Chart Builder',
|
||
component: ChartBuilderModal,
|
||
isUrlNavigable: true,
|
||
dismissOnBackdrop: false,
|
||
init: (datasetId) => useChartBuilderStore.getState().init(datasetId ? Number(datasetId) : null),
|
||
},
|
||
|
||
// Pure info modals — no state, no validity check, no discard prompt on close.
|
||
// `about` is URL-navigable (reload-restore); `donate` is not (spec §01E hash
|
||
// table omits both, but `about` is still a permanent, bookmark-worthy surface).
|
||
about: {
|
||
name: 'about',
|
||
title: 'About & Help',
|
||
component: AboutModal,
|
||
},
|
||
donate: {
|
||
name: 'donate',
|
||
title: 'Donate',
|
||
component: DonateModal,
|
||
},
|
||
};
|
||
|
||
export const getModalConfig = (name: ActiveModal): ModalConfig | undefined =>
|
||
name ? MODAL_REGISTRY[name] : undefined;
|
||
|
||
export const getModalTitle = (name: ActiveModal): string => getModalConfig(name)?.title ?? '';
|
||
|
||
export const isUrlNavigable = (name: ActiveModal): boolean =>
|
||
getModalConfig(name)?.isUrlNavigable ?? false;
|