From 71b2a55231ea6f57775e869ace23d86f3be80070 Mon Sep 17 00:00:00 2001 From: sanjibdevnathlabs Date: Tue, 17 Mar 2026 18:55:11 +0530 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(fonts):=20extract?= =?UTF-8?q?=20font=20families=20to=20shared=20JSON=20single=20source=20of?= =?UTF-8?q?=20truth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Font family IDs were duplicated across 5 files (types.ts, setup.ts, index.ts, SKILL.md, preferences.example.json) with inconsistent mappings — Comic Shanns was 4 in some places but actually 8 in Excalidraw source. This caused wrong fonts to render on canvas. Fix: create src/font-families.json as the canonical font data, import it in types.ts, and derive all other references from it. Static docs now point to the JSON file instead of duplicating the mapping. Co-Authored-By: Claude Opus 4.6 --- skills/excalidraw-skill/SKILL.md | 8 ++-- .../excalidraw-skill/preferences.example.json | 12 +---- src/font-families.json | 13 +++++ src/index.ts | 10 ++-- src/setup.ts | 17 +++---- src/types.ts | 48 ++++++++++++------- 6 files changed, 62 insertions(+), 46 deletions(-) create mode 100644 src/font-families.json diff --git a/skills/excalidraw-skill/SKILL.md b/skills/excalidraw-skill/SKILL.md index 8e723e6..93fb44c 100644 --- a/skills/excalidraw-skill/SKILL.md +++ b/skills/excalidraw-skill/SKILL.md @@ -26,7 +26,7 @@ Before creating any elements, load the user's diagram preferences. These control | 1 (highest) | Session | In-memory (set via prompt during this conversation) | No — current session only | | 2 | Folder | `.claude/excalidraw-preferences.json` in the current project root | Yes — per-project | | 3 | Global | `~/.claude/skills/excalidraw-skill/preferences.json` | Yes — all projects | -| 4 (lowest) | Hardcoded | Server defaults (fontFamily: 1, roughness: 0, fontSize: 20, strokeWidth: 2) | — | +| 4 (lowest) | Hardcoded | Server defaults (fontFamily: 5, roughness: 0, fontSize: 20, strokeWidth: 2) | — | ### How to Load @@ -45,11 +45,11 @@ If no preferences file exists at either location, **prompt the user before drawi Ask these questions (use `AskUserQuestion` tool if available, otherwise ask inline): -1. **Font family** — Which font for all text? - - Excalifont (hand-drawn) = 1 +1. **Font family** — Which font for all text? _(IDs from `src/font-families.json`)_ + - Excalifont (hand-drawn) = 5 - Helvetica (sans-serif) = 2 - Cascadia (monospace) = 3 - - Comic Shanns = 4 + - Comic Shanns = 8 - Nunito = 6 - Lilita One = 7 diff --git a/skills/excalidraw-skill/preferences.example.json b/skills/excalidraw-skill/preferences.example.json index 8998b09..be8c9ed 100644 --- a/skills/excalidraw-skill/preferences.example.json +++ b/skills/excalidraw-skill/preferences.example.json @@ -1,16 +1,8 @@ { "_comment": "Excalidraw MCP user preferences. Copy to preferences.json to activate.", - "_fontReference": { - "1": "Excalifont (hand-drawn)", - "2": "Helvetica (sans-serif)", - "3": "Cascadia (monospace)", - "4": "Comic Shanns", - "5": "Liberation Sans", - "6": "Nunito", - "7": "Lilita One" - }, + "_fontReference": "See src/font-families.json for canonical font ID → name mapping.", "defaults": { - "fontFamily": 1, + "fontFamily": 5, "fontSize": 20, "roughness": 0, "strokeWidth": 2 diff --git a/src/font-families.json b/src/font-families.json new file mode 100644 index 0000000..3d9fa0e --- /dev/null +++ b/src/font-families.json @@ -0,0 +1,13 @@ +{ + "fonts": [ + { "id": 5, "name": "Excalifont", "label": "Excalifont (hand-drawn)", "aliases": ["excalifont", "hand-drawn"] }, + { "id": 2, "name": "Helvetica", "label": "Helvetica (sans-serif)", "aliases": ["helvetica", "arial", "sans-serif"] }, + { "id": 3, "name": "Cascadia", "label": "Cascadia (monospace)", "aliases": ["cascadia", "monospace", "courier"] }, + { "id": 8, "name": "Comic Shanns", "label": "Comic Shanns", "aliases": ["comic shanns", "comic sans"] }, + { "id": 6, "name": "Nunito", "label": "Nunito", "aliases": ["nunito"] }, + { "id": 7, "name": "Lilita One", "label": "Lilita One", "aliases": ["lilita one"] }, + { "id": 9, "name": "Liberation Sans", "label": "Liberation Sans", "aliases": ["liberation sans"], "legacy": true }, + { "id": 1, "name": "Virgil", "label": "Virgil (legacy)", "aliases": ["virgil"], "legacy": true } + ], + "defaultFontFamily": 5 +} diff --git a/src/index.ts b/src/index.ts index 1b3f0c2..0f6ba61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,7 +27,9 @@ import { ExcalidrawElementType, validateElement, normalizeFontFamily, - files as globalFiles + files as globalFiles, + DEFAULT_FONT_FAMILY, + FONT_FAMILY_DESCRIPTION, } from './types.js'; import fetch from 'node-fetch'; import { startCanvasServer, stopCanvasServer } from './server.js'; @@ -75,7 +77,7 @@ interface ExcalidrawPreferences { } const HARDCODED_DEFAULTS: ExcalidrawPreferences = { - fontFamily: 1, + fontFamily: DEFAULT_FONT_FAMILY, fontSize: 20, roughness: 0, strokeWidth: 2, @@ -459,7 +461,7 @@ const tools: Tool[] = [ opacity: { type: 'number' }, text: { type: 'string' }, fontSize: { type: 'number' }, - fontFamily: { type: ['string', 'number'], description: 'Font family: 1=Excalifont (hand-drawn), 2=Helvetica (sans-serif), 3=Cascadia (monospace), 4=Comic Shanns, 5=Liberation Sans, 6=Nunito, 7=Lilita One. Accepts name strings too.' }, + fontFamily: { type: ['string', 'number'], description: FONT_FAMILY_DESCRIPTION }, startElementId: { type: 'string', description: 'For arrows: ID of the element to bind the arrow start to. Arrow auto-routes to element edge.' }, endElementId: { type: 'string', description: 'For arrows: ID of the element to bind the arrow end to. Arrow auto-routes to element edge.' }, endArrowhead: { type: 'string', description: 'Arrowhead style at end: arrow, bar, dot, triangle, or null' }, @@ -690,7 +692,7 @@ const tools: Tool[] = [ opacity: { type: 'number' }, text: { type: 'string' }, fontSize: { type: 'number' }, - fontFamily: { type: ['string', 'number'], description: 'Font family: 1=Excalifont, 2=Helvetica, 3=Cascadia, 4=Comic Shanns, 5=Liberation Sans, 6=Nunito, 7=Lilita One. Accepts name strings too.' }, + fontFamily: { type: ['string', 'number'], description: FONT_FAMILY_DESCRIPTION }, startElementId: { type: 'string', description: 'For arrows: ID of element to bind arrow start to' }, endElementId: { type: 'string', description: 'For arrows: ID of element to bind arrow end to' }, endArrowhead: { type: 'string', description: 'Arrowhead style at end: arrow, bar, dot, triangle, or null' }, diff --git a/src/setup.ts b/src/setup.ts index 4eb053d..6d4c2c3 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -13,6 +13,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { execSync } from 'child_process'; +import { FONT_FAMILIES, DEFAULT_FONT_FAMILY } from './types.js'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); @@ -184,14 +185,10 @@ async function phaseEnvironment(rl: readline.Interface): Promise { // ── Preference Setup ───────────────────────────────────────── -const FONT_OPTIONS: { value: number; label: string }[] = [ - { value: 1, label: 'Excalifont (hand-drawn)' }, - { value: 2, label: 'Helvetica (sans-serif)' }, - { value: 3, label: 'Cascadia (monospace)' }, - { value: 4, label: 'Comic Shanns' }, - { value: 6, label: 'Nunito' }, - { value: 7, label: 'Lilita One' }, -]; +// Derived from FONT_FAMILIES in types.ts — single source of truth +const FONT_OPTIONS = FONT_FAMILIES + .filter(f => !f.legacy) + .map(f => ({ value: f.id, label: f.label })); const ROUGHNESS_OPTIONS: { value: number; label: string }[] = [ { value: 0, label: 'Clean / professional' }, @@ -245,12 +242,12 @@ async function phasePreferences(rl: readline.Interface, phaseLabel: string): Pro // Font process.stdout.write('\n Font family:\n'); FONT_OPTIONS.forEach((f, i) => { - const marker = f.value === 1 ? ' (default)' : ''; + const marker = f.value === DEFAULT_FONT_FAMILY ? ' (default)' : ''; process.stdout.write(` ${CYAN}[${i + 1}]${RESET} ${f.label}${marker}\n`); }); const fontAnswer = (await ask(rl, 'Choose [1]: ')).trim(); const fontIdx = fontAnswer === '' ? 0 : parseInt(fontAnswer, 10) - 1; - const fontFamily = (fontIdx >= 0 && fontIdx < FONT_OPTIONS.length) ? FONT_OPTIONS[fontIdx]!.value : 1; + const fontFamily = (fontIdx >= 0 && fontIdx < FONT_OPTIONS.length) ? FONT_OPTIONS[fontIdx]!.value : DEFAULT_FONT_FAMILY; // Roughness process.stdout.write('\n Diagram style:\n'); diff --git a/src/types.ts b/src/types.ts index 8f6b2aa..5bc6ccd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -311,24 +311,36 @@ export interface ExcalidrawFile { // In-memory file storage (image files are too large for SQLite row storage) export const files = new Map(); -// Font family normalization: Excalidraw expects numeric IDs, but agents -// often send string names. Map common names to their numeric equivalents. -const FONT_FAMILY_MAP: Record = { - 'virgil': 1, - 'hand-drawn': 1, - 'excalifont': 1, - 'helvetica': 2, - 'arial': 2, - 'sans-serif': 2, - 'cascadia': 3, - 'monospace': 3, - 'courier': 3, - 'comic shanns': 4, - 'comic sans': 4, - 'liberation sans': 5, - 'nunito': 6, - 'lilita one': 7, -}; +// ── Font families — single source of truth ────────────────────────────── +// IDs match the @excalidraw/excalidraw FONT_FAMILY constant. +// The canonical data lives in font-families.json; every other file derives from it. +import fontData from './font-families.json' with { type: 'json' }; + +export interface FontFamilyDef { + id: number; + name: string; + label: string; + aliases: string[]; + legacy?: boolean; // hidden from setup menus / tool docs +} + +export const FONT_FAMILIES: FontFamilyDef[] = fontData.fonts as FontFamilyDef[]; + +export const DEFAULT_FONT_FAMILY: number = fontData.defaultFontFamily; + +// Derived: description string for MCP tool schemas +export const FONT_FAMILY_DESCRIPTION = + 'Font family: ' + + FONT_FAMILIES.filter(f => !f.legacy).map(f => `${f.id}=${f.name}`).join(', ') + + '. Accepts name strings too.'; + +// Derived: string → number mapping for normalization +const FONT_FAMILY_MAP: Record = {}; +for (const font of FONT_FAMILIES) { + for (const alias of font.aliases) { + FONT_FAMILY_MAP[alias] = font.id; + } +} export function normalizeFontFamily(value: string | number | undefined): number | undefined { if (value === undefined || value === null) return undefined;