Eng-council sweep: export hygiene, dead code, helper dedup, declare vega-expression

This commit is contained in:
2026-06-12 19:39:54 +03:00
parent f26ac66633
commit 0e225d7d5c
26 changed files with 89 additions and 98 deletions
+10 -20
View File
@@ -14,9 +14,16 @@
import { useMemo, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { datasetReference, tabularRows, type DataSource, type Dataset } from '@core/dataset';
import {
cellText,
datasetReference,
tabularRows,
type DataSource,
type Dataset,
} from '@core/dataset';
import { detectFormat, detectFormatFromUrl, type DataFormat } from '@core/format-detection';
import { datasetUsageCounts, snippetsReferencingDataset } from '@core/relationships';
import { humanizeBytes } from '@core/storage-estimate';
import { closeModal, openModal, resnapshot } from '../modals/ModalCoordinator';
import { fetchRemoteData } from '../infrastructure/remote-data';
import { remoteFetchErrorMessage } from '../services/remote-data-errors';
@@ -38,15 +45,6 @@ function formatLabel(format: DataFormat): string {
return format === 'topojson' ? 'TopoJSON' : format.toUpperCase();
}
/** Human-readable byte size (B / KB / MB). */
function humanBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
const kb = bytes / 1024;
if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;
const mb = kb / 1024;
return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
}
const SOURCE_OPTIONS: ReadonlyArray<SegmentedOption<DataSource>> = [
{ value: 'inline', label: 'Inline' },
{ value: 'url', label: 'URL' },
@@ -55,14 +53,6 @@ const SOURCE_OPTIONS: ReadonlyArray<SegmentedOption<DataSource>> = [
/** Rows shown in the tabular preview before truncating (spec §05 → Detail Panel). */
const PREVIEW_ROW_LIMIT = 50;
/** One table cell's text: blank for empty, the string as-is, else JSON (numbers,
* booleans, nested values). Avoids `String()` on objects ("[object Object]"). */
function cellText(value: unknown): string {
if (value == null) return '';
if (typeof value === 'string') return value;
return JSON.stringify(value);
}
export function DatasetsModal() {
const datasets = useDatasetStore(useShallow((s) => s.datasets));
const view = useDatasetStore((s) => s.view);
@@ -137,7 +127,7 @@ function DatasetListItem({
if (unfetched) parts.push('not fetched');
else if (dataset.rowCount !== null) parts.push(`${dataset.rowCount} rows`);
parts.push(formatLabel(dataset.format));
if (!unfetched) parts.push(humanBytes(dataset.size));
if (!unfetched) parts.push(humanizeBytes(dataset.size));
return (
<li className={`${styles.item} ${active ? styles.itemActive : ''}`}>
@@ -324,7 +314,7 @@ function DatasetDetail({
</div>
<div>
<dt>Size</dt>
<dd>{dataset.data == null ? 'N/A' : humanBytes(dataset.size)}</dd>
<dd>{dataset.data == null ? 'N/A' : humanizeBytes(dataset.size)}</dd>
</div>
</dl>
+1 -1
View File
@@ -47,7 +47,7 @@ export type IconName =
| 'status-info'; // Carbon InformationFilled
/** Carbon icon scale (arch 09 §5.3). 16px (sm) is the default, paired to 14px body. */
export type IconSize = 'sm' | 'md' | 'lg' | 'xl';
type IconSize = 'sm' | 'md' | 'lg' | 'xl';
/** Inner SVG geometry per glyph, on Carbon's 0 0 32 32 grid. fill comes from CSS. */
const GLYPHS: Record<IconName, ReactNode> = {
-3
View File
@@ -99,9 +99,6 @@ function tx<T>(
);
}
export const get = <T>(store: string, key: IDBValidKey): Promise<T | undefined> =>
tx<T | undefined>(store, 'readonly', (s) => s.get(key) as IDBRequest<T | undefined>);
export const getAll = <T>(store: string): Promise<T[]> =>
tx<T[]>(store, 'readonly', (s) => s.getAll() as IDBRequest<T[]>);
+5
View File
@@ -87,6 +87,11 @@ export function readView(): ViewState {
return parseHash(window.location.hash);
}
/** The current raw hash, for comparing a serialized view against the URL. */
export function currentHash(): string {
return window.location.hash;
}
/**
* Build the next URL for `view`. The hash is the app's only view-state channel,
* so we normalize to a hash-only canonical URL: any stray query string (a shared
+1 -1
View File
@@ -48,7 +48,7 @@ export function resnapshot(): void {
}
/** True when the active modal's editable state differs from its baseline. */
export function hasUnsavedChanges(): boolean {
function hasUnsavedChanges(): boolean {
const name = useAppStore.getState().activeModal;
if (!name) return false;
const current = snapshotOf(name);
+2 -7
View File
@@ -34,6 +34,7 @@ import { useChartBuilderStore } from '../stores/ChartBuilderStore';
import { useDatasetStore } from '../stores/DatasetStore';
import { useSnippetStore } from '../stores/SnippetStore';
import {
currentHash,
pushView,
readView,
replaceView,
@@ -72,7 +73,7 @@ export function deriveViewState(): ViewState {
/** True when `view` already matches the current URL hash. */
function isCurrent(view: ViewState): boolean {
return serializeHash(view) === window.location.hash;
return serializeHash(view) === currentHash();
}
/**
@@ -174,12 +175,6 @@ export function startRouting(): void {
useChartBuilderStore.subscribe(syncUrlFromState);
}
/** Test seam: reset the module's start/applying flags between cases. */
export function resetRoutingForTest(): void {
started = false;
applying = false;
}
// --- Coordinator seam (unchanged call sites) -------------------------------
// The coordinator calls these on modal open/close; both now just re-derive the
// hash from state. Kept as named exports so ModalCoordinator needs no edit.
+1 -1
View File
@@ -17,7 +17,7 @@ import {
import { usePanesStore } from '../stores/PanesStore';
/** Delay before a settled resize is persisted. */
export const PANES_PERSIST_DEBOUNCE_MS = 300;
const PANES_PERSIST_DEBOUNCE_MS = 300;
/** Hydrate persisted pane widths + visibility into the store. Call before render. */
export function initPanes(): void {
+2 -2
View File
@@ -12,7 +12,7 @@ import type { VisualizationSpec } from 'vega-embed';
import type { Config } from 'vega-lite';
/** Options for `RenderHandle.toImageURL` (spec §08 → Per-chart export). */
export interface ImageExportOptions {
interface ImageExportOptions {
/**
* Pixel-density multiplier for the **PNG** raster, **relative to the device**.
* The image is drawn at `scale × devicePixelRatio` logical-pixel density, so
@@ -80,7 +80,7 @@ export interface RenderOptions {
* size against this (÷ dpr, since the backing store is dpr× the CSS size) and
* throws `ChartTooLargeError` rather than handing back a blank canvas.
*/
export const MAX_CANVAS_PX = 32767;
const MAX_CANVAS_PX = 32767;
/**
* Thrown by `renderSpec` when a **canvas**-backed chart resolves to a height larger
+24 -19
View File
@@ -110,12 +110,20 @@ export function runMergeChartTheme(editor: monaco.editor.IStandaloneCodeEditor):
});
}
/** Remove the draft's `config` block, copying it to the clipboard first. */
export async function runExtractConfig(editor: monaco.editor.IStandaloneCodeEditor): Promise<void> {
/**
* Shared prologue of the two extract actions: the model, the spec parsed off it,
* and the config split out — or null (after toasting) when any step has nothing
* to work with.
*/
function extractableConfig(editor: monaco.editor.IStandaloneCodeEditor): {
model: monaco.editor.ITextModel;
rest: Record<string, unknown>;
config: Record<string, unknown>;
} | null {
const model = editor.getModel();
if (!model) return;
if (!model) return null;
const spec = parseSpecObject(model);
if (!spec) return;
if (!spec) return null;
const { spec: rest, config } = extractConfigFromSpec(spec);
if (config === null) {
@@ -124,8 +132,16 @@ export async function runExtractConfig(editor: monaco.editor.IStandaloneCodeEdit
title: 'No config to extract',
message: 'This spec has no config block.',
});
return;
return null;
}
return { model, rest, config };
}
/** Remove the draft's `config` block, copying it to the clipboard first. */
export async function runExtractConfig(editor: monaco.editor.IStandaloneCodeEditor): Promise<void> {
const extracted = extractableConfig(editor);
if (!extracted) return;
const { model, rest, config } = extracted;
// Copy before removing — if the clipboard write fails, the spec keeps its
// config and nothing is lost.
@@ -157,20 +173,9 @@ export async function runExtractConfig(editor: monaco.editor.IStandaloneCodeEdit
* non-overlapping keys stop applying once the new theme replaces it.)
*/
export function runExtractConfigToTheme(editor: monaco.editor.IStandaloneCodeEditor): void {
const model = editor.getModel();
if (!model) return;
const spec = parseSpecObject(model);
if (!spec) return;
const { spec: rest, config } = extractConfigFromSpec(spec);
if (config === null) {
notify({
kind: 'info',
title: 'No config to extract',
message: 'This spec has no config block.',
});
return;
}
const extracted = extractableConfig(editor);
if (!extracted) return;
const { model, rest, config } = extracted;
const snippet = selectActiveSnippet(useSnippetStore.getState());
const theme = useCustomThemeStore
+2 -8
View File
@@ -27,6 +27,7 @@ import {
reassignCollidingSnippetIds,
} from '@core/import-normalize';
import { snippetSizeBytes } from '@core/snippet';
import { humanizeBytes } from '@core/storage-estimate';
import { downloadJson, readTextFile } from '../infrastructure/file-transfer';
import { deleteSnippet, saveSnippet, StorageQuotaError } from '../infrastructure/snippet-store';
import { notify } from '../stores/NotificationStore';
@@ -36,13 +37,6 @@ import { useSnippetStore } from '../stores/SnippetStore';
/** Practical snippet-storage budget (spec §02 storage monitor / §08). */
const SNIPPET_BUDGET_BYTES = 5 * 1024 * 1024;
/** Round bytes to a short KB/MB string for the overage warning. */
function formatBytes(bytes: number): string {
const kb = bytes / 1024;
if (kb < 1024) return `${Math.max(1, Math.round(kb))} KB`;
return `${Math.round(kb / 1024)} MB`;
}
/**
* Export the whole workspace to a downloaded JSON file (spec §08 → Export).
* Flushes the live editor buffer first so in-progress draft edits are included.
@@ -209,7 +203,7 @@ export async function importWorkspace(file: File): Promise<void> {
}
if (overage > 0) {
clauses.push(
`This puts snippet storage about ${formatBytes(overage)} over the ~5 MB budget; ` +
`This puts snippet storage about ${humanizeBytes(overage)} over the ~5 MB budget; ` +
`consider deleting some snippets.`,
);
}
-5
View File
@@ -14,11 +14,6 @@ import type { ModalName } from '../modals/types';
* docs/architecture/01-state-and-stores.md.
*/
export type { UiTheme };
// The modal union is defined once in the modal system (docs/architecture/03) and
// re-exported here for the many callers that reach it through the app store.
export type { ModalName };
export interface AppState {
/** Active UI theme; mirrored onto <html data-theme> by a subscriber. */
uiTheme: UiTheme;
+1 -1
View File
@@ -25,7 +25,7 @@ import { customThemeSelection } from '@core/vega-themes';
import { useAppStore } from './AppStore';
/** The Theme Builder's in-progress edit of the selected theme. */
export interface ThemeDraft {
interface ThemeDraft {
name: string;
/** The config as editable JSON text (pretty-printed on load). */
configText: string;
+2 -2
View File
@@ -9,7 +9,7 @@
* Two layers of action live here:
* - Low-level mutators (`add`/`update`/`remove`) are the single place the
* `datasets` array changes; the persistence subscriber writes them through to
* IndexedDB, and services (RelationshipService, future import) reuse them.
* IndexedDB, and services (e.g. the import flow in services/transfer) reuse them.
* - Form orchestration (`save`) validates the form (name required + unique,
* data detectable) and turns it into an `add`/`update`, keeping the modal
* component thin.
@@ -31,7 +31,7 @@ import { isNameTaken } from '@core/naming';
import { useSnippetStore } from './SnippetStore';
/** Which pane the Datasets manager is showing (spec §05 → Layout). */
export type DatasetView = 'list' | 'detail' | 'new' | 'edit';
type DatasetView = 'list' | 'detail' | 'new' | 'edit';
/** The in-progress create/edit form. `input` is the paste area (inline) or URL field. */
export interface DatasetForm {
+1 -1
View File
@@ -31,7 +31,7 @@ interface InlineData {
* none (no snippet, unparseable, or no `data.values`). The format comes from an
* explicit `data.format.type` when present (raw CSV/TSV strings), else JSON.
*/
export function readInlineData(draftText: string): InlineData | null {
function readInlineData(draftText: string): InlineData | null {
let parsed: unknown;
try {
parsed = JSON.parse(draftText);
+1 -1
View File
@@ -178,7 +178,7 @@ export function shownSideWidths(
/** Per-pane visibility (spec §01A). Widths are kept *independently* of visibility,
* so a hidden pane keeps its remembered width and re-shows at it (not a default). */
export interface PaneVisibility {
interface PaneVisibility {
library?: boolean;
editor?: boolean;
preview?: boolean;