mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Modal system: break import cycles — component map moves to the shell
This commit is contained in:
@@ -56,7 +56,7 @@ import {
|
||||
type TimeUnit,
|
||||
} from '@core/chart-builder';
|
||||
import { referencedFields, validateExpression } from '@core/expr-validate';
|
||||
import { tabularRows } from '@core/dataset';
|
||||
import { cellText, tabularRows } from '@core/dataset';
|
||||
import type { ColumnType } from '@core/type-inference';
|
||||
import { DatasetNotFoundError, prepareSpecForRender } from '@core/rendering';
|
||||
import { chartConfigFor } from '@core/vega-themes';
|
||||
@@ -179,13 +179,6 @@ const PREVIEW_ROW_LIMIT = 50;
|
||||
*/
|
||||
const VEGA_EXPRESSION_DOCS_URL = 'https://vega.github.io/vega/docs/expressions/';
|
||||
|
||||
/** One preview cell's text: blank for empty, the string as-is, else JSON. */
|
||||
function cellText(value: unknown): string {
|
||||
if (value == null) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
/** A safe `datum` accessor for a column name (dot for identifiers, bracket otherwise). */
|
||||
function datumRef(name: string): string {
|
||||
return /^[A-Za-z_$][\w$]*$/.test(name) ? `datum.${name}` : `datum[${JSON.stringify(name)}]`;
|
||||
@@ -1425,7 +1418,10 @@ export function ChartBuilderModal() {
|
||||
className={`${styles.action} ${styles.primary}`}
|
||||
disabled={!valid}
|
||||
aria-describedby={!valid ? 'cb-create-hint' : undefined}
|
||||
onClick={() => runCreate()}
|
||||
// The create is the user's confirmation — close with no discard prompt.
|
||||
onClick={() => {
|
||||
if (runCreate()) void closeModal(true);
|
||||
}}
|
||||
>
|
||||
Create Snippet
|
||||
</button>
|
||||
|
||||
@@ -2,23 +2,43 @@
|
||||
* Modal shell (docs/architecture/03 → Layer 3).
|
||||
*
|
||||
* Renders exactly ONE modal — whichever `activeModal` names — inside a single
|
||||
* reusable chrome: backdrop, header (title + close), and a focus trap. The
|
||||
* modal's registered `component` fills the body. This is the only place a modal
|
||||
* name maps to a view (`<Body />` from the registry), so there is no
|
||||
* `name === 'datasets' && <DatasetsModal/>` chain anywhere.
|
||||
* reusable chrome: backdrop, header (title + close), and a focus trap. The body
|
||||
* comes from `MODAL_COMPONENTS` below — the only place a modal name maps to a
|
||||
* view, so there is no `name === 'datasets' && <DatasetsModal/>` chain anywhere.
|
||||
* The map lives here rather than in the registry so the registry (which the
|
||||
* coordinator and URL sync import) never imports components — that edge would
|
||||
* close an import cycle: coordinator → registry → component → coordinator.
|
||||
*
|
||||
* Dismissal is uniform for these passive feature modals: the close button,
|
||||
* Escape, or a backdrop click — never a click inside the body (which stops
|
||||
* propagation). Each modal owns its own action buttons; the shell stays generic.
|
||||
*/
|
||||
|
||||
import type { ComponentType } from 'react';
|
||||
import { useAppStore } from '../stores/AppStore';
|
||||
import { getModalConfig, getModalTitle } from '../modals/modal-registry';
|
||||
import type { ModalName } from '../modals/types';
|
||||
import { closeModal } from '../modals/ModalCoordinator';
|
||||
import { useFocusTrap } from '../hooks/useFocusTrap';
|
||||
import { AboutModal } from './AboutModal';
|
||||
import { ChartBuilderModal } from './ChartBuilderModal';
|
||||
import { DatasetsModal } from './DatasetsModal';
|
||||
import { DonateModal } from './DonateModal';
|
||||
import { ExtractModal } from './ExtractModal';
|
||||
import { ThemeBuilderModal } from './ThemeBuilderModal';
|
||||
import { Icon } from './Icon';
|
||||
import styles from './ModalShell.module.css';
|
||||
|
||||
/** The body rendered for each modal name (metadata stays in the registry). */
|
||||
const MODAL_COMPONENTS: Record<ModalName, ComponentType> = {
|
||||
datasets: DatasetsModal,
|
||||
extract: ExtractModal,
|
||||
chartBuilder: ChartBuilderModal,
|
||||
themeBuilder: ThemeBuilderModal,
|
||||
about: AboutModal,
|
||||
donate: DonateModal,
|
||||
};
|
||||
|
||||
export function ModalShell() {
|
||||
const name = useAppStore((s) => s.activeModal);
|
||||
const config = getModalConfig(name);
|
||||
@@ -40,8 +60,8 @@ export function ModalShell() {
|
||||
isLarge ? '#modal-title' : undefined,
|
||||
);
|
||||
|
||||
if (!config) return null;
|
||||
const Body = config.component;
|
||||
if (!config || !name) return null;
|
||||
const Body = MODAL_COMPONENTS[name];
|
||||
|
||||
// Modals with in-progress work (the Chart Builder's config) opt out of
|
||||
// click-outside-to-close so an accidental backdrop click can't discard it; Escape
|
||||
|
||||
Reference in New Issue
Block a user