Build: landing/learn shed eager Monaco+Vega; post-build gate + immutable asset caching

This commit is contained in:
2026-07-02 22:34:24 +03:00
parent fb4b42a1a7
commit 552900dac5
11 changed files with 170 additions and 77 deletions
+2 -7
View File
@@ -20,13 +20,8 @@
import type { ReactNode } from 'react';
import type { JsonObject } from '@core/spec-config';
import {
type ConfigPath,
countSet,
getConfigValue,
schemeColors,
schemesByKind,
} from '@core/theme-controls';
import { type ConfigPath, countSet, getConfigValue, schemesByKind } from '@core/theme-controls';
import { schemeColors } from '@core/scheme-colors';
import { Button } from './Button';
import { ColorField } from './ColorField';
import { Icon } from './Icon';
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { schemeColors } from './scheme-colors';
describe('schemeColors', () => {
it('returns the full fixed palette for a categorical scheme (count ignored)', () => {
const colors = schemeColors('tableau10', 3);
expect(colors).toHaveLength(10);
expect(colors.every((c) => /^#[0-9a-f]{6}$/i.test(c))).toBe(true);
});
it('samples a continuous scheme at count stops, normalized to hex', () => {
const colors = schemeColors('viridis', 5);
expect(colors).toHaveLength(5);
expect(colors.every((c) => /^#[0-9a-f]{6}$/.test(c))).toBe(true);
});
it('samples the midpoint for a single continuous stop', () => {
expect(schemeColors('viridis', 1)).toHaveLength(1);
});
it('returns [] for an unknown scheme', () => {
expect(schemeColors('not-a-scheme')).toEqual([]);
});
});
+40
View File
@@ -0,0 +1,40 @@
/**
* Named-scheme resolution to hex swatches (docs/exploration/chart-theming-scope.md §5).
*
* Split from theme-controls.ts because of the `vega-scale` import below:
* theme-controls is reachable from the landing/learn entry graphs (via
* `vega-themes`), and a static vega-scale edge there drags the vega vendor
* chunk into the marketing pages' eager JS (`scripts/check-light-entries.mjs`
* guards this after every build). Only the app's color controls resolve
* schemes to swatches, so the edge lives here, app-side of the split.
*/
import { scheme } from 'vega-scale';
/**
* Resolve a named Vega scheme to hex swatches. A categorical scheme returns its
* fixed palette in full; a continuous scheme (sequential/diverging) is sampled
* at `count` evenly-spaced stops (`count` applies to continuous schemes only).
* Continuous interpolators yield `rgb(...)`, normalized to hex here. An unknown
* name returns `[]` — the picker shows that scheme without a preview rather than
* throwing. Used for the swatch/gradient preview and "materialize to swatches".
*/
export function schemeColors(name: string, count = 9): string[] {
const resolved: unknown = scheme(name);
if (Array.isArray(resolved)) return resolved.map((c) => toHex(String(c)));
if (typeof resolved === 'function' && count > 0) {
const interp = resolved as (t: number) => string;
if (count === 1) return [toHex(interp(0.5))];
return Array.from({ length: count }, (_, i) => toHex(interp(i / (count - 1))));
}
return [];
}
/** Normalize a CSS color to `#rrggbb`; passes through existing hex and unknowns. */
function toHex(color: string): string {
if (color.startsWith('#')) return color;
const m = /rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i.exec(color);
if (!m) return color;
const h = (n: string) => Math.round(Number(n)).toString(16).padStart(2, '0');
return `#${h(m[1])}${h(m[2])}${h(m[3])}`;
}
+1 -23
View File
@@ -8,10 +8,10 @@ import {
enumValue,
getConfigValue,
normalizeRangeSchemes,
schemeColors,
schemesByKind,
setConfigValue,
} from './theme-controls';
import { schemeColors } from './scheme-colors';
describe('getConfigValue', () => {
const config = { range: { category: ['#111', '#222'] }, mark: { color: '#333' } };
@@ -163,28 +163,6 @@ describe('leaf coercion', () => {
});
});
describe('schemeColors', () => {
it('returns the full fixed palette for a categorical scheme (count ignored)', () => {
const colors = schemeColors('tableau10', 3);
expect(colors).toHaveLength(10);
expect(colors.every((c) => /^#[0-9a-f]{6}$/i.test(c))).toBe(true);
});
it('samples a continuous scheme at count stops, normalized to hex', () => {
const colors = schemeColors('viridis', 5);
expect(colors).toHaveLength(5);
expect(colors.every((c) => /^#[0-9a-f]{6}$/.test(c))).toBe(true);
});
it('samples the midpoint for a single continuous stop', () => {
expect(schemeColors('viridis', 1)).toHaveLength(1);
});
it('returns [] for an unknown scheme', () => {
expect(schemeColors('not-a-scheme')).toEqual([]);
});
});
describe('enumValue', () => {
const options = [{ value: '' }, { value: 'start' }, { value: 'end' }] as const;
+8 -40
View File
@@ -1,9 +1,8 @@
/**
* Theme Builder structured-control primitives (docs/chart-theming-scope.md §5).
* Theme Builder structured-control primitives (docs/exploration/chart-theming-scope.md §5).
*
* Pure core: the read/write transforms the builder's structured controls run on
* a draft config, plus the named-color-scheme catalog and its resolution. Three
* concerns:
* a draft config, plus the named-color-scheme catalog. Two concerns:
*
* - **Path get/set** — read a value at a nested config path, and set one back
* immutably while preserving every sibling key at each level. That guarantee
@@ -15,16 +14,15 @@
* writes a minimal diff, never a default dump.
* - **Scheme catalog** — the named Vega color schemes the color controls offer,
* grouped by kind (categorical / sequential / diverging).
* - **Scheme resolution** (`schemeColors`) — a scheme name to hex swatches, for
* the picker preview and the "materialize to an editable array" action.
*
* `vega-scale` (a focused vega sub-package, like core's `vega-expression`) owns
* the scheme registry; importing it here keeps the umbrella `vega` out of core.
* The one control transform that predates this module, `applyFontToConfig`,
* stays in custom-theme.ts with the record entity.
* Scheme *resolution* (name → hex swatches) lives in `scheme-colors.ts`: it
* imports the `vega-scale` registry, and this module is reachable from the
* landing/learn entry graphs (via `vega-themes`), which must stay free of
* static edges into the vega vendor chunk. The one control transform that
* predates this module, `applyFontToConfig`, stays in custom-theme.ts with the
* record entity.
*/
import { scheme } from 'vega-scale';
import { isJsonObject, type JsonObject } from './spec-config';
// ── Path get/set ──────────────────────────────────────────────────────────
@@ -213,33 +211,3 @@ export const THEME_SCHEMES: ReadonlyArray<ThemeScheme> = [
export function schemesByKind(kind: SchemeKind): ThemeScheme[] {
return THEME_SCHEMES.filter((s) => s.kind === kind);
}
// ── Scheme resolution ───────────────────────────────────────────────────────
/**
* Resolve a named Vega scheme to hex swatches. A categorical scheme returns its
* fixed palette in full; a continuous scheme (sequential/diverging) is sampled
* at `count` evenly-spaced stops (`count` applies to continuous schemes only).
* Continuous interpolators yield `rgb(...)`, normalized to hex here. An unknown
* name returns `[]` — the picker shows that scheme without a preview rather than
* throwing. Used for the swatch/gradient preview and "materialize to swatches".
*/
export function schemeColors(name: string, count = 9): string[] {
const resolved: unknown = scheme(name);
if (Array.isArray(resolved)) return resolved.map((c) => toHex(String(c)));
if (typeof resolved === 'function' && count > 0) {
const interp = resolved as (t: number) => string;
if (count === 1) return [toHex(interp(0.5))];
return Array.from({ length: count }, (_, i) => toHex(interp(i / (count - 1))));
}
return [];
}
/** Normalize a CSS color to `#rrggbb`; passes through existing hex and unknowns. */
function toHex(color: string): string {
if (color.startsWith('#')) return color;
const m = /rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i.exec(color);
if (!m) return color;
const h = (n: string) => Math.round(Number(n)).toString(16).padStart(2, '0');
return `#${h(m[1])}${h(m[2])}${h(m[3])}`;
}