forked from c-fu/arah2
feat(01-design-tokens-rams-foundation-03): Rams cartography basemap — liberty fork derived from token package
- build.mjs: fetches OpenFreeMap liberty style, parses theme/src/tokens.css, derives the full role palette in-script via desaturate(tokenHex, satMultiplier, lightnessPerRole) — no hand-tuned cartography hex (D-09) - liberty-rams.json (110 layers): grayscale/muted land + water, desaturated roads (hierarchy by weight only), muted matte-black labels at reduced opacity, zero-radius aesthetic enforced (halo-blur zeroed, building-3d faux-3D removed) (D-08) - accent guard: build fails if #EB8008/#ED3F1C/#736B1E (or rgb forms) leak into layers — route accent reserved for Phase 5 - D-10 font fallback: Space Grotesk/Geist probed on glyphs endpoint (404) → Noto Sans Bold/Regular substituted, recorded in style.metadata rams-font-substitutions - raster relief desaturated (raster-saturation -1); derivation provenance embedded in style.metadata rams-cartography - theme/package.json: basemap script
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* araharah — Rams cartography basemap builder (D-08 / D-09 / D-10)
|
||||
* ------------------------------------------------------------------
|
||||
* Forks the OpenFreeMap `liberty` vector style into the Telemetry
|
||||
* Rams cartography: calibrated grayscale/muted grounding, desaturated
|
||||
* roads (hierarchy by weight only), muted labels, zero accent colors.
|
||||
*
|
||||
* D-09: every cartographic value is DERIVED from the token package
|
||||
* (theme/src/tokens.css) inside this script — no hand-tuned hex in
|
||||
* the style JSON. The only literals below are the derivation
|
||||
* parameters (saturation multipliers, lightness targets per role)
|
||||
* that shape token genes into cartographic roles.
|
||||
*
|
||||
* D-08: signal-orange / functional-red / olive-green are reserved
|
||||
* for the route line + hazard semantics rendered by later phases
|
||||
* (Phase 5 owns the route vector). The accent guard at the bottom of
|
||||
* this script fails the build if any of the three accents leak into
|
||||
* the output layers.
|
||||
*
|
||||
* D-10: intended Rams label stacks (Space Grotesk for major place
|
||||
* labels, Geist for minor) are probed on the OpenFreeMap glyphs
|
||||
* endpoint at build time. The endpoint does not self-host them in
|
||||
* Phase 1 (pbf glyph ranges), so the closest HOSTED stack is
|
||||
* substituted and recorded in style.metadata["rams-font-substitutions"]
|
||||
* — honest fallback provenance; Phase 5+ may move to self-hosted
|
||||
* glyphs served from the Worker.
|
||||
*
|
||||
* Node >= 18 (global fetch). No dependencies beyond node builtins.
|
||||
*
|
||||
* Usage: npm run basemap --workspace theme
|
||||
*/
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const TOKENS_CSS = join(HERE, '../src/tokens.css'); // D-09 derivation source
|
||||
const OUT_JSON = join(HERE, 'liberty-rams.json');
|
||||
const LIBERTY_URL = 'https://tiles.openfreemap.org/styles/liberty';
|
||||
|
||||
/* ———————————————————— color math (pure helpers) ———————————————————— */
|
||||
|
||||
const SAT_BAND = [0.06, 0.12]; // calibrated low-saturation band (Interfaces)
|
||||
|
||||
const hexToRgb = (hex) => {
|
||||
const n = Number.parseInt(hex.slice(1), 16);
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||||
};
|
||||
|
||||
const rgbToHex = ([r, g, b]) =>
|
||||
'#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('').toUpperCase();
|
||||
|
||||
/** average two token hexes — "between soft-beige and chassis-subtle" */
|
||||
const mixHex = (a, b) => {
|
||||
const A = hexToRgb(a);
|
||||
const B = hexToRgb(b);
|
||||
return rgbToHex(A.map((v, i) => Math.round((v + B[i]) / 2)));
|
||||
};
|
||||
|
||||
const rgbToHsl = ([r0, g0, b0]) => {
|
||||
const r = r0 / 255, g = g0 / 255, b = b0 / 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h = 0, s = 0;
|
||||
const l = (max + min) / 2;
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
default: h = (r - g) / d + 4;
|
||||
}
|
||||
h *= 60;
|
||||
}
|
||||
return [h, s, l];
|
||||
};
|
||||
|
||||
const hslToRgb = ([hDeg, s, l]) => {
|
||||
const h = hDeg / 360;
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
const f = (t0) => {
|
||||
const t = (t0 % 1 + 1) % 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
return [f(h + 1 / 3), f(h), f(h - 1 / 3)].map((v) => Math.round(v * 255));
|
||||
};
|
||||
|
||||
/**
|
||||
* desaturate(hex, satMultiplier, lightness?) — the D-09 derivation core.
|
||||
* hex→HSL, multiply saturation, clamp into the calibrated band
|
||||
* [0.06, 0.12], then adjust lightness to the role target (Interfaces
|
||||
* table). Hue genes from the source token are preserved — the whole
|
||||
* palette stays one warm-neutral instrument family.
|
||||
*/
|
||||
const desaturate = (hex, satMultiplier, lightness) => {
|
||||
const [h, s, l] = rgbToHsl(hexToRgb(hex));
|
||||
const sat = Math.min(Math.max(s * satMultiplier, SAT_BAND[0]), SAT_BAND[1]);
|
||||
const lig = lightness === undefined ? l : lightness;
|
||||
return rgbToHex(hslToRgb([h, sat, lig]));
|
||||
};
|
||||
|
||||
/** token hex → rgba() string (labels, halos — matte-black at reduced opacity) */
|
||||
const rgba = (hex, alpha) => {
|
||||
const [r, g, b] = hexToRgb(hex);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
/* ———————————————————— token parsing (D-09) ———————————————————— */
|
||||
|
||||
const TOKEN_RE = /--color-([a-z0-9-]+):\s*(#[0-9A-Fa-f]{6})/g;
|
||||
|
||||
function parseTokens(css) {
|
||||
const map = new Map();
|
||||
for (const m of css.matchAll(TOKEN_RE)) map.set(m[1], m[2].toUpperCase());
|
||||
return map;
|
||||
}
|
||||
|
||||
/* Accent tokens are excluded from the derivation set by name AND by
|
||||
* value (primary-container aliases the signal-orange hex) — D-08. */
|
||||
const EXCLUDED_TOKENS = new Set(['signal-orange', 'functional-red', 'olive-green']);
|
||||
const ACCENT_HEXES = new Set(['#EB8008', '#ED3F1C', '#736B1E']);
|
||||
const ACCENT_RGB_RES = [
|
||||
/\brgba?\(\s*235\s*,\s*128\s*,\s*8\b/i, // signal-orange
|
||||
/\brgba?\(\s*237\s*,\s*63\s*,\s*28\b/i, // functional-red
|
||||
/\brgba?\(\s*115\s*,\s*107\s*,\s*30\b/i, // olive-green
|
||||
];
|
||||
|
||||
function token(tokens, name) {
|
||||
const hex = tokens.get(name);
|
||||
if (!hex) throw new Error(`token not found in tokens.css: --color-${name}`);
|
||||
if (EXCLUDED_TOKENS.has(name)) {
|
||||
throw new Error(`accent token excluded from basemap derivation: ${name} (D-08)`);
|
||||
}
|
||||
if (ACCENT_HEXES.has(hex)) {
|
||||
throw new Error(`token ${name} carries an accent hex (${hex}) excluded from derivation (D-08)`);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
/* ———————————————————— role palette (all derived) ———————————————————— */
|
||||
|
||||
function buildRoles(tokens) {
|
||||
const T = (name) => token(tokens, name);
|
||||
const roles = {
|
||||
/* land ground: between soft-beige and chassis-subtle (Interfaces) */
|
||||
ground: desaturate(mixHex(T('soft-beige'), T('chassis-subtle')), 0.4),
|
||||
/* parcel step slightly darker than ground (parks, landcover, landuse) */
|
||||
groundSubtle: desaturate(T('chassis-subtle'), 0.45),
|
||||
/* mid-dark rule for parcel outlines / park rings */
|
||||
groundEdge: desaturate(T('matte-black'), 1, 0.62),
|
||||
/* water: desaturated mid-gray derived from matte-black (Interfaces) */
|
||||
water: desaturate(T('matte-black'), 1, 0.71),
|
||||
/* minor road cores — lightest neutral */
|
||||
roadFillMinor: desaturate(T('pure-white'), 1, 0.96),
|
||||
/* major road cores — slightly darker than minor; hierarchy by weight */
|
||||
roadFillMajor: desaturate(T('surface-container-low'), 0.12, 0.92),
|
||||
/* machined dark rule around road cores (1px/2px solid rules per design doc) */
|
||||
casing: desaturate(T('matte-black'), 1, 0.34),
|
||||
rail: desaturate(T('matte-black'), 1, 0.48),
|
||||
/* buildings: light gray within the calibrated band, darker than ground */
|
||||
buildingFill: desaturate(T('soft-beige'), 0.5, 0.76),
|
||||
buildingEdge: desaturate(T('matte-black'), 1, 0.68),
|
||||
boundary: desaturate(T('matte-black'), 1, 0.4),
|
||||
aeroway: desaturate(T('chassis-subtle'), 0.5, 0.7),
|
||||
};
|
||||
const derivations = {
|
||||
ground: 'mix(soft-beige, chassis-subtle) · sat ×0.4 · band [0.06, 0.12]',
|
||||
groundSubtle: 'chassis-subtle · sat ×0.45 (parcel step darker than ground)',
|
||||
groundEdge: 'matte-black · sat ×1 (band-min) · L 0.62',
|
||||
water: 'matte-black · sat ×1 (band-min) · L 0.71 (mid-gray)',
|
||||
roadFillMinor: 'pure-white · sat ×1 (band-min) · L 0.96',
|
||||
roadFillMajor: 'surface-container-low · sat ×0.12 · L 0.92 (slightly darker than minor)',
|
||||
casing: 'matte-black · sat ×1 (band-min) · L 0.34 (machined rule)',
|
||||
rail: 'matte-black · sat ×1 (band-min) · L 0.48',
|
||||
buildingFill: 'soft-beige · sat ×0.5 · L 0.76',
|
||||
buildingEdge: 'matte-black · sat ×1 (band-min) · L 0.68',
|
||||
boundary: 'matte-black · sat ×1 (band-min) · L 0.40',
|
||||
aeroway: 'chassis-subtle · sat ×0.5 · L 0.70',
|
||||
};
|
||||
return { roles, derivations };
|
||||
}
|
||||
|
||||
/* Labels: matte-black at reduced opacity (Interfaces); halo = ground tone */
|
||||
function buildLabelColors(roles, tokens) {
|
||||
const matte = token(tokens, 'matte-black'); // allowed — not an accent
|
||||
return {
|
||||
minor: rgba(matte, 0.6),
|
||||
major: rgba(matte, 0.82),
|
||||
halo: rgba(roles.ground, 0.9),
|
||||
};
|
||||
}
|
||||
|
||||
/* ———————————————————— layer classification ———————————————————— */
|
||||
|
||||
/* Major place labels: city and above ("place-city, place-subtown and
|
||||
* above" — liberty has no subtown layer; city-class and everything
|
||||
* above it in the place hierarchy). Town/village/other, POIs, water
|
||||
* names, highway names/shields, airports → minor. */
|
||||
const MAJOR_LABEL_IDS = new Set([
|
||||
'label_country_1',
|
||||
'label_country_2',
|
||||
'label_country_3',
|
||||
'label_state',
|
||||
'label_city',
|
||||
'label_city_capital',
|
||||
]);
|
||||
|
||||
const ROAD_BODY_MAJOR = new Set(['motorway', 'motorway_link', 'trunk_primary', 'secondary_tertiary', 'link']);
|
||||
const ROAD_BODY_MINOR = new Set(['minor', 'street', 'service_track', 'path_pedestrian']);
|
||||
|
||||
function classifyRoadBody(body) {
|
||||
if (/^(major_rail|transit_rail)(_hatching)?$/.test(body)) return 'rail';
|
||||
if (body.endsWith('_casing')) return 'casing';
|
||||
if (ROAD_BODY_MAJOR.has(body)) return 'roadFillMajor';
|
||||
if (ROAD_BODY_MINOR.has(body)) return 'roadFillMinor';
|
||||
return null; // road_area_pattern / one-way arrow symbols — untouched
|
||||
}
|
||||
|
||||
function classify(layer) {
|
||||
const id = layer.id;
|
||||
|
||||
/* all label-bearing symbols first (waterway_line_label etc.) */
|
||||
if (layer.type === 'symbol') {
|
||||
if (layer.layout && Array.isArray(layer.layout['text-font'])) {
|
||||
return { kind: 'label', major: MAJOR_LABEL_IDS.has(id) };
|
||||
}
|
||||
return null; // icon-only symbols (one-way arrows) — untouched
|
||||
}
|
||||
|
||||
if (id === 'background') return { kind: 'role', role: 'ground' };
|
||||
if (id === 'building') return { kind: 'role', role: 'buildingFill', outline: 'buildingEdge' };
|
||||
if (id === 'water' || id.startsWith('waterway_')) return { kind: 'role', role: 'water' };
|
||||
if (id === 'park') return { kind: 'role', role: 'groundSubtle', outline: 'groundEdge' };
|
||||
if (id === 'park_outline') return { kind: 'role', role: 'groundEdge' };
|
||||
if (id.startsWith('landcover_') || id.startsWith('landuse_') || id === 'aeroway_fill') {
|
||||
return { kind: 'role', role: 'groundSubtle', outline: 'groundEdge' };
|
||||
}
|
||||
if (id.startsWith('aeroway_')) return { kind: 'role', role: 'aeroway' };
|
||||
if (id.startsWith('boundary_')) return { kind: 'role', role: 'boundary' };
|
||||
if (/^(road|tunnel|bridge)_/.test(id)) {
|
||||
const role = classifyRoadBody(id.replace(/^(road|tunnel|bridge)_/, ''));
|
||||
return role ? { kind: 'role', role } : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ———————————————————— transform ———————————————————— */
|
||||
|
||||
function transformLayer(layer, ctx) {
|
||||
if (layer.id === 'natural_earth') {
|
||||
/* shaded-relief raster → fully desaturated (grayscale grounding) */
|
||||
layer.paint = layer.paint || {};
|
||||
layer.paint['raster-saturation'] = -1;
|
||||
ctx.touched.push(`${layer.id} → raster-saturation −1`);
|
||||
return layer;
|
||||
}
|
||||
|
||||
const spec = classify(layer);
|
||||
if (!spec) {
|
||||
ctx.untouched.push(layer.id);
|
||||
return layer;
|
||||
}
|
||||
|
||||
const p = (layer.paint = layer.paint || {});
|
||||
|
||||
if (spec.kind === 'role') {
|
||||
/* zoom-interpolated color ramps flatten to the role constant;
|
||||
* width/opacity ramps are preserved — hierarchy by weight only */
|
||||
if ('background-color' in p) p['background-color'] = ctx.roles[spec.role];
|
||||
if ('fill-color' in p) p['fill-color'] = ctx.roles[spec.role];
|
||||
if ('fill-outline-color' in p) p['fill-outline-color'] = ctx.roles[spec.outline || spec.role];
|
||||
if ('line-color' in p) p['line-color'] = ctx.roles[spec.role];
|
||||
ctx.touched.push(`${layer.id} → ${spec.role}`);
|
||||
}
|
||||
|
||||
if (spec.kind === 'label') {
|
||||
/* muted labels: matte-black at reduced opacity, ground-tone halo */
|
||||
p['text-color'] = spec.major ? ctx.labelColors.major : ctx.labelColors.minor;
|
||||
p['text-halo-color'] = ctx.labelColors.halo;
|
||||
if ('text-halo-blur' in p) p['text-halo-blur'] = 0; // Rams: zero blur
|
||||
layer.layout['text-font'] = (spec.major ? ctx.fonts.served.major : ctx.fonts.served.minor).slice();
|
||||
ctx.touched.push(`${layer.id} → label(${spec.major ? 'major' : 'minor'})`);
|
||||
}
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
/* ———————————————————— font probing (D-10 fallback) ———————————————————— */
|
||||
|
||||
const FONT_INTENT = {
|
||||
major: ['Space Grotesk Medium'], // Rams headline face for city-and-above
|
||||
minor: ['Geist Regular', 'Space Grotesk Regular'], // Rams body face for the rest
|
||||
};
|
||||
const FONT_FALLBACK = {
|
||||
major: ['Noto Sans Bold'], // closest hosted stack to the headline-weight role
|
||||
minor: ['Noto Sans Regular'],
|
||||
};
|
||||
|
||||
async function probeGlyph(glyphsUrl, stack) {
|
||||
const url = glyphsUrl
|
||||
.replace('{fontstack}', encodeURIComponent(stack))
|
||||
.replace('{range}', '0-255');
|
||||
try {
|
||||
const res = await fetch(url, { redirect: 'follow' });
|
||||
return res.ok; // 404 → false → substitution path
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveFontStacks(glyphsUrl) {
|
||||
const served = {};
|
||||
const substitutions = {};
|
||||
|
||||
for (const [kind, intentStacks] of Object.entries(FONT_INTENT)) {
|
||||
const hosted = [];
|
||||
for (const stack of intentStacks) {
|
||||
if (await probeGlyph(glyphsUrl, stack)) hosted.push(stack);
|
||||
}
|
||||
if (hosted.length > 0) {
|
||||
served[kind] = hosted;
|
||||
} else {
|
||||
for (const stack of FONT_FALLBACK[kind]) {
|
||||
if (!(await probeGlyph(glyphsUrl, stack))) {
|
||||
throw new Error(`fallback glyph stack not hosted either: ${stack}`);
|
||||
}
|
||||
}
|
||||
served[kind] = FONT_FALLBACK[kind];
|
||||
substitutions[kind] = {
|
||||
intended: intentStacks,
|
||||
served: FONT_FALLBACK[kind],
|
||||
reason:
|
||||
'not hosted on tiles.openfreemap.org glyphs endpoint (HTTP 404 at build time); ' +
|
||||
'closest hosted stack substituted. Self-hosted Rams woff2 (theme/fonts) cannot ' +
|
||||
'serve pbf glyph ranges in Phase 1 — revisit with a Worker-served glyphs route.',
|
||||
};
|
||||
}
|
||||
}
|
||||
return { served, substitutions };
|
||||
}
|
||||
|
||||
/* ———————————————————— accent guard (D-08) ———————————————————— */
|
||||
|
||||
function assertNoAccent(style) {
|
||||
const json = JSON.stringify(style.layers);
|
||||
for (const hex of ACCENT_HEXES) {
|
||||
if (json.toUpperCase().includes(hex)) {
|
||||
throw new Error(`accent leak: ${hex} present in output layers (D-08 violation)`);
|
||||
}
|
||||
}
|
||||
for (const re of ACCENT_RGB_RES) {
|
||||
if (re.test(json)) {
|
||||
throw new Error(`accent leak (rgb form): ${re} matched output layers (D-08 violation)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ———————————————————— main ———————————————————— */
|
||||
|
||||
async function main() {
|
||||
const [tokensCss, styleRes] = await Promise.all([
|
||||
readFile(TOKENS_CSS, 'utf8'),
|
||||
fetch(LIBERTY_URL, { redirect: 'follow' }),
|
||||
]);
|
||||
if (!styleRes.ok) throw new Error(`liberty style fetch failed: HTTP ${styleRes.status}`);
|
||||
const style = await styleRes.json();
|
||||
|
||||
const tokens = parseTokens(tokensCss);
|
||||
if (tokens.size < 40) throw new Error(`token parse suspiciously thin: ${tokens.size} tokens`);
|
||||
const { roles, derivations } = buildRoles(tokens);
|
||||
const labelColors = buildLabelColors(roles, tokens);
|
||||
const fonts = await resolveFontStacks(style.glyphs);
|
||||
|
||||
/* glyphs endpoint kept as hosted OpenFreeMap (Phase 1: no self-hosted pbf) */
|
||||
if (!style.glyphs || !style.glyphs.includes('tiles.openfreemap.org/fonts')) {
|
||||
throw new Error(`unexpected glyphs endpoint: ${style.glyphs}`);
|
||||
}
|
||||
|
||||
const ctx = { roles, labelColors, fonts, touched: [], untouched: [] };
|
||||
const removedLayers = [];
|
||||
|
||||
const layers = [];
|
||||
for (const layer of style.layers) {
|
||||
/* building-3d is fill-extrusion — faux 3D dimensionality is
|
||||
* rejected by the Rams design system (Elevation & Depth) */
|
||||
if (layer.id === 'building-3d') {
|
||||
removedLayers.push({
|
||||
id: layer.id,
|
||||
reason: 'fill-extrusion = faux 3D — rejected by Telemetry Rams (Elevation & Depth: Physical Planarity)',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
layers.push(transformLayer(layer, ctx));
|
||||
}
|
||||
style.layers = layers;
|
||||
|
||||
style.name = 'Telemetry Rams Basemap';
|
||||
style.metadata = {
|
||||
...(style.metadata || {}),
|
||||
'rams-cartography': {
|
||||
'derived-from': 'theme/src/tokens.css',
|
||||
'derivation-mechanism': 'desaturate(tokenHex, satMultiplier, lightnessPerRole) — hue genes from tokens, saturation clamped to [0.06, 0.12]',
|
||||
'source-style': LIBERTY_URL,
|
||||
roles: Object.fromEntries(
|
||||
Object.entries(roles).map(([k, v]) => [k, { color: v, derivation: derivations[k] }])
|
||||
),
|
||||
'excluded-accent-tokens': [...EXCLUDED_TOKENS],
|
||||
'untouched-layers': ctx.untouched,
|
||||
},
|
||||
'rams-font-substitutions': fonts.substitutions,
|
||||
'rams-removed-layers': removedLayers,
|
||||
};
|
||||
|
||||
assertNoAccent(style);
|
||||
|
||||
const out = JSON.stringify(style, null, 2) + '\n';
|
||||
await writeFile(OUT_JSON, out, 'utf8');
|
||||
|
||||
/* build report — provenance for human review */
|
||||
console.log('Telemetry Rams basemap — derivation report');
|
||||
console.log(` tokens parsed: ${tokens.size} from ${TOKENS_CSS}`);
|
||||
console.log(' role palette:');
|
||||
for (const [k, v] of Object.entries(roles)) console.log(` ${k.padEnd(15)} ${v} (${derivations[k]})`);
|
||||
console.log(` labels: major ${labelColors.major} / minor ${labelColors.minor} / halo ${labelColors.halo}`);
|
||||
console.log(` fonts (served): major ${fonts.served.major} / minor ${fonts.served.minor}`);
|
||||
if (Object.keys(fonts.substitutions).length > 0) {
|
||||
for (const [kind, s] of Object.entries(fonts.substitutions)) {
|
||||
console.log(` font substitution: [${kind}] ${s.intended.join(', ')} → ${s.served.join(', ')}`);
|
||||
}
|
||||
}
|
||||
console.log(` layers: ${ctx.touched.length} transformed, ${ctx.untouched.length} untouched (${ctx.untouched.join(', ')}), ${removedLayers.length} removed`);
|
||||
console.log(' accent guard: PASS (no #EB8008 / #ED3F1C / #736B1E in layers)');
|
||||
console.log(` written: ${OUT_JSON} (${(out.length / 1024).toFixed(1)} KB)`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`basemap build failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -11,6 +11,7 @@
|
||||
"scripts": {
|
||||
"build": "tailwindcss -i ./src/theme.css -o ./dist/rams.css --minify",
|
||||
"send": "mkdir -p dist/fonts && cp -r fonts/geist fonts/space-grotesk dist/fonts/",
|
||||
"sheet": "npm run build && echo 'open theme/src/sheet.html in browser ( Consumes theme/dist/rams.css )'"
|
||||
"sheet": "npm run build && echo 'open theme/src/sheet.html in browser ( Consumes theme/dist/rams.css )'",
|
||||
"basemap": "node basemap/build.mjs"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user