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;