diff --git a/oxlint.json b/oxlint.json index 4840cbb50..18e397162 100644 --- a/oxlint.json +++ b/oxlint.json @@ -38,6 +38,7 @@ "allowImportNames": [ "loadFont", "listFamilies", + "listFonts", "preloadFonts" ] }, diff --git a/packages/core/src/figma-api/index.ts b/packages/core/src/figma-api/index.ts index 600578d1e..5978d2b3a 100644 --- a/packages/core/src/figma-api/index.ts +++ b/packages/core/src/figma-api/index.ts @@ -1,7 +1,14 @@ import { IS_BROWSER } from '../constants' import { computeBounds } from '../geometry' import { copyFills, copyStrokes, copyEffects } from '../scene-graph/copy' -import { FigmaNodeProxy, INTERNAL_ID, MIXED, type FigmaFontName, type NodeProxyHost } from './proxy' +import { + FigmaNodeProxy, + INTERNAL_ID, + MIXED, + type FigmaFont, + type FigmaFontName, + type NodeProxyHost +} from './proxy' import type { RasterExportFormat } from '../io/formats/raster' import type { @@ -15,7 +22,7 @@ import type { import type { Rect, Vector } from '../types' export { FigmaNodeProxy } from './proxy' -export type { FigmaFontName } from './proxy' +export type { FigmaFont, FigmaFontName } from './proxy' export function computeImageHash(data: Uint8Array): string { let h1 = 0x811c9dc5 >>> 0 @@ -370,6 +377,12 @@ export class FigmaAPI implements NodeProxyHost { // No-op: we don't gate text editing on font loading } + async listAvailableFontsAsync(): Promise { + // Default: pure browser / test contexts have no enumeration surface. + // Desktop hosts override this to return system + bundled fonts. + return [] + } + notify(message: string): { cancel: () => void } { if (typeof console !== 'undefined') console.log(`[figma.notify] ${message}`) // eslint-disable-next-line no-empty-function diff --git a/packages/core/src/figma-api/proxy.ts b/packages/core/src/figma-api/proxy.ts index 9d0f7c891..c8012cb2c 100644 --- a/packages/core/src/figma-api/proxy.ts +++ b/packages/core/src/figma-api/proxy.ts @@ -19,6 +19,7 @@ import type { Rect } from '../types' const MIXED = Symbol('mixed') export type FigmaFontName = { family: string; style: string } +export type FigmaFont = { fontName: FigmaFontName } export function weightToStyleName(weight: number, italic: boolean): string { const base = FONT_WEIGHT_NAMES[weight] ?? 'Regular' diff --git a/packages/core/src/tools/read.ts b/packages/core/src/tools/read.ts index cd2ae67d7..bc561d28c 100644 --- a/packages/core/src/tools/read.ts +++ b/packages/core/src/tools/read.ts @@ -227,6 +227,27 @@ export const listFonts = defineTool({ } }) +export const listAvailableFonts = defineTool({ + name: 'list_available_fonts', + description: + 'List font families the host can render (system fonts on desktop plus any bundled fonts). ' + + 'Use this to discover what fonts are available to set on a text node — distinct from list_fonts ' + + 'which only reports families currently used in the page.', + params: { + family: { type: 'string', description: 'Filter by family name (substring, case-insensitive)' } + }, + execute: async (figma, args) => { + const fonts = await figma.listAvailableFontsAsync() + let families = Array.from(new Set(fonts.map((f) => f.fontName.family))) + if (args.family) { + const q = args.family.toLowerCase() + families = families.filter((f) => f.toLowerCase().includes(q)) + } + families.sort((a, b) => a.localeCompare(b)) + return { count: families.length, fonts: families } + } +}) + export const queryNodes = defineTool({ name: 'query_nodes', description: `Query nodes using XPath selectors. Node types are element names (FRAME, TEXT, RECTANGLE, ELLIPSE, etc.). Attributes: name, width, height, x, y, visible, opacity, cornerRadius, fontSize, fontFamily, fontWeight, layoutMode, itemSpacing, paddingTop/Right/Bottom/Left, strokeWeight, rotation, locked, blendMode, text, lineHeight, letterSpacing. diff --git a/packages/core/src/tools/registry.ts b/packages/core/src/tools/registry.ts index 89389d2f4..3bf3ddcc8 100644 --- a/packages/core/src/tools/registry.ts +++ b/packages/core/src/tools/registry.ts @@ -58,6 +58,7 @@ import { pageBounds, selectNodes, listFonts, + listAvailableFonts, getJsx, diffJsx } from './read' @@ -164,6 +165,7 @@ export const EXTENDED_TOOLS: ToolDef[] = [ switchPage, pageBounds, listFonts, + listAvailableFonts, diffJsx, // Create (advanced) createShape, diff --git a/src/automation/figma-factory.ts b/src/automation/figma-factory.ts index b08f5063f..9854e4038 100644 --- a/src/automation/figma-factory.ts +++ b/src/automation/figma-factory.ts @@ -1,5 +1,6 @@ import { FigmaAPI } from '@open-pencil/core' +import { listFonts } from '@/engine/fonts' import type { EditorStore } from '@/stores/editor' export function makeFigmaFromStore(store: EditorStore): FigmaAPI { @@ -17,5 +18,11 @@ export function makeFigmaFromStore(store: EditorStore): FigmaAPI { } api.exportImage = (nodeIds, opts) => store.renderExportImage(nodeIds, opts.scale ?? 1, opts.format ?? 'PNG') + api.listAvailableFontsAsync = async () => { + const fonts = await listFonts() + return fonts.flatMap(({ family, styles }) => + styles.map((style) => ({ fontName: { family, style } })) + ) + } return api } diff --git a/src/engine/fonts.ts b/src/engine/fonts.ts index 328d92cea..2ebdb8599 100644 --- a/src/engine/fonts.ts +++ b/src/engine/fonts.ts @@ -52,6 +52,13 @@ export async function listFamilies(): Promise { return coreList() } +export async function listFonts(): Promise { + if (IS_TAURI) { + return getTauriFonts() + } + return [] +} + export async function loadFont(family: string, style = 'Regular'): Promise { if (IS_TAURI) { try {