inkhouse 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +201 -0
  2. package/bin/inkhouse.mjs +171 -0
  3. package/code.js +11802 -0
  4. package/manifest.json +30 -0
  5. package/package.json +45 -0
  6. package/scanner/blob-placement-regression.ts +132 -0
  7. package/scanner/class-collector.ts +69 -0
  8. package/scanner/cli.ts +336 -0
  9. package/scanner/component-scanner.ts +2876 -0
  10. package/scanner/css-patch-regression.ts +112 -0
  11. package/scanner/css-token-reader-regression.ts +92 -0
  12. package/scanner/css-token-reader.ts +477 -0
  13. package/scanner/font-style-resolver-regression.ts +32 -0
  14. package/scanner/index.ts +9 -0
  15. package/scanner/radial-gradient-regression.ts +53 -0
  16. package/scanner/style-map.ts +145 -0
  17. package/scanner/tailwind-parser.ts +644 -0
  18. package/scanner/transform-math-regression.ts +42 -0
  19. package/scanner/types.ts +298 -0
  20. package/src/blob-placement.ts +111 -0
  21. package/src/change-detection.ts +204 -0
  22. package/src/class-utils.ts +105 -0
  23. package/src/clip-path-decorative.ts +194 -0
  24. package/src/color-resolver.ts +98 -0
  25. package/src/colors.ts +196 -0
  26. package/src/component-defs.ts +54 -0
  27. package/src/component-gen.ts +561 -0
  28. package/src/component-lookup.ts +82 -0
  29. package/src/config.ts +115 -0
  30. package/src/design-system.ts +59 -0
  31. package/src/dev-server.ts +173 -0
  32. package/src/figma-globals.d.ts +3 -0
  33. package/src/font-style-resolver.ts +171 -0
  34. package/src/github.ts +1465 -0
  35. package/src/icon-builder.ts +607 -0
  36. package/src/image-cache.ts +22 -0
  37. package/src/inline-text.ts +271 -0
  38. package/src/layout-parser.ts +667 -0
  39. package/src/layout-utils.ts +155 -0
  40. package/src/main.ts +687 -0
  41. package/src/node-ir.ts +595 -0
  42. package/src/pack-provider.ts +148 -0
  43. package/src/packs.ts +126 -0
  44. package/src/radial-gradient.ts +84 -0
  45. package/src/render-context.ts +138 -0
  46. package/src/responsive-analyzer.ts +139 -0
  47. package/src/state-analyzer.ts +143 -0
  48. package/src/story-builder.ts +1706 -0
  49. package/src/story-layout.ts +38 -0
  50. package/src/tailwind.ts +2379 -0
  51. package/src/text-builder.ts +116 -0
  52. package/src/text-line.ts +42 -0
  53. package/src/token-source.ts +43 -0
  54. package/src/tokens.ts +717 -0
  55. package/src/transform-math.ts +44 -0
  56. package/src/ui-builder.ts +1996 -0
  57. package/src/utility-resolver.ts +125 -0
  58. package/src/variables.ts +1042 -0
  59. package/src/width-solver.ts +466 -0
  60. package/templates/patch-tokens-route.ts +165 -0
  61. package/templates/scan-components-route.ts +57 -0
  62. package/ui.html +1222 -0
package/src/config.ts ADDED
@@ -0,0 +1,115 @@
1
+ // Figma Plugin API types are provided by the runtime
2
+
3
+ import type { TokenSourceMode } from './token-source';
4
+
5
+ export interface ProjectConfig {
6
+ owner: string;
7
+ repo: string;
8
+ baseBranch: string;
9
+ tokenPath: string;
10
+ tokenSourceMode: TokenSourceMode;
11
+ cssTokenPath: string;
12
+ syncDtcgOnPush: boolean;
13
+ allowNewTokensFromFigma: boolean;
14
+ newTokenPrefixes: string[];
15
+ projectName: string;
16
+ }
17
+
18
+ // Default GitHub configuration (can be overridden via Settings)
19
+ export const DEFAULT_CONFIG: ProjectConfig = {
20
+ owner: '',
21
+ repo: '',
22
+ baseBranch: 'main',
23
+ tokenPath: 'design-tokens/tokens.dtcg.json',
24
+ tokenSourceMode: 'auto',
25
+ cssTokenPath: '',
26
+ syncDtcgOnPush: false,
27
+ allowNewTokensFromFigma: false,
28
+ newTokenPrefixes: [],
29
+ projectName: 'My Project',
30
+ };
31
+
32
+ // Runtime config - loaded from storage
33
+ export let GITHUB_CONFIG: ProjectConfig | null = null;
34
+
35
+ function normalizeTokenPath(tokenPath: unknown): string {
36
+ if (typeof tokenPath !== 'string' || !tokenPath.trim()) {
37
+ return DEFAULT_CONFIG.tokenPath;
38
+ }
39
+ if (tokenPath === 'design-tokens/tokens.json') {
40
+ return DEFAULT_CONFIG.tokenPath;
41
+ }
42
+ return tokenPath;
43
+ }
44
+
45
+ function normalizeTokenSourceMode(mode: unknown): TokenSourceMode {
46
+ if (mode === 'css' || mode === 'dtcg' || mode === 'auto') return mode;
47
+ return 'auto';
48
+ }
49
+
50
+ function normalizeCssTokenPath(cssTokenPath: unknown): string {
51
+ if (typeof cssTokenPath !== 'string') return '';
52
+ return cssTokenPath.trim();
53
+ }
54
+
55
+ function normalizeSyncDtcgOnPush(syncDtcgOnPush: unknown): boolean {
56
+ return syncDtcgOnPush === true;
57
+ }
58
+
59
+ function normalizeAllowNewTokensFromFigma(value: unknown): boolean {
60
+ return value === true;
61
+ }
62
+
63
+ function normalizeNewTokenPrefixes(value: unknown): string[] {
64
+ if (Array.isArray(value)) {
65
+ return value
66
+ .map((entry) => String(entry || '').trim())
67
+ .filter((entry) => entry.length > 0);
68
+ }
69
+ if (typeof value === 'string') {
70
+ return value
71
+ .split(',')
72
+ .map((entry) => entry.trim())
73
+ .filter((entry) => entry.length > 0);
74
+ }
75
+ return [];
76
+ }
77
+
78
+ // Load config from storage or use defaults
79
+ export async function loadConfig(): Promise<ProjectConfig> {
80
+ if (GITHUB_CONFIG) return GITHUB_CONFIG;
81
+ try {
82
+ const stored = await figma.clientStorage.getAsync('project_config');
83
+ if (stored && typeof stored === 'object') {
84
+ const storedConfig = stored as Partial<ProjectConfig>;
85
+ GITHUB_CONFIG = {
86
+ ...DEFAULT_CONFIG,
87
+ ...storedConfig,
88
+ tokenPath: normalizeTokenPath(storedConfig.tokenPath),
89
+ tokenSourceMode: normalizeTokenSourceMode(storedConfig.tokenSourceMode),
90
+ cssTokenPath: normalizeCssTokenPath(storedConfig.cssTokenPath),
91
+ syncDtcgOnPush: normalizeSyncDtcgOnPush(storedConfig.syncDtcgOnPush),
92
+ allowNewTokensFromFigma: normalizeAllowNewTokensFromFigma((storedConfig as any).allowNewTokensFromFigma),
93
+ newTokenPrefixes: normalizeNewTokenPrefixes((storedConfig as any).newTokenPrefixes),
94
+ };
95
+ // Persist one-time migrations so old installs stop carrying stale settings.
96
+ await figma.clientStorage.setAsync('project_config', GITHUB_CONFIG);
97
+ } else {
98
+ GITHUB_CONFIG = Object.assign({}, DEFAULT_CONFIG);
99
+ }
100
+ } catch (e) {
101
+ GITHUB_CONFIG = Object.assign({}, DEFAULT_CONFIG);
102
+ }
103
+ return GITHUB_CONFIG;
104
+ }
105
+
106
+ export async function saveConfig(config: ProjectConfig): Promise<void> {
107
+ config.tokenPath = normalizeTokenPath(config.tokenPath);
108
+ config.tokenSourceMode = normalizeTokenSourceMode((config as any).tokenSourceMode);
109
+ config.cssTokenPath = normalizeCssTokenPath((config as any).cssTokenPath);
110
+ config.syncDtcgOnPush = normalizeSyncDtcgOnPush((config as any).syncDtcgOnPush);
111
+ (config as any).allowNewTokensFromFigma = normalizeAllowNewTokensFromFigma((config as any).allowNewTokensFromFigma);
112
+ (config as any).newTokenPrefixes = normalizeNewTokenPrefixes((config as any).newTokenPrefixes);
113
+ GITHUB_CONFIG = config;
114
+ await figma.clientStorage.setAsync('project_config', config);
115
+ }
@@ -0,0 +1,59 @@
1
+ // Design System Page Generation
2
+ // Orchestrates building the full design system preview page in Figma.
3
+
4
+ import { debug } from './colors';
5
+ import { getThemeNames, TOKENS } from './tokens';
6
+ import { demoFrameColors, demoFrameRadii } from './variables';
7
+ import { createUIComponents } from './ui-builder';
8
+
9
+ /**
10
+ * Build a single "Design System" page containing tokens preview and all UI components.
11
+ * Clears any existing content on the page before rebuilding.
12
+ */
13
+ export function buildDesignSystemSinglePage(): void {
14
+ let ds: any = (figma.root as any).children.find((p: any) => p.name === 'Design System');
15
+ if (!ds) { ds = figma.createPage(); ds.name = 'Design System'; }
16
+ figma.currentPage = ds;
17
+ const nodes = Array.from(ds.children || []);
18
+ for (const n of nodes as any[]) n.remove();
19
+
20
+ const tokensRow = figma.createFrame();
21
+ tokensRow.name = 'Design Tokens';
22
+ tokensRow.layoutMode = 'HORIZONTAL';
23
+ tokensRow.primaryAxisSizingMode = 'AUTO';
24
+ tokensRow.counterAxisSizingMode = 'AUTO';
25
+ tokensRow.itemSpacing = 32;
26
+ tokensRow.paddingLeft = tokensRow.paddingRight = 32;
27
+ tokensRow.paddingTop = tokensRow.paddingBottom = 24;
28
+ tokensRow.fills = [];
29
+ tokensRow.strokes = [];
30
+ const themeNames = getThemeNames(TOKENS);
31
+ for (const themeName of themeNames) {
32
+ tokensRow.appendChild(demoFrameColors(themeName));
33
+ }
34
+ tokensRow.appendChild(demoFrameRadii());
35
+ tokensRow.x = 48;
36
+ tokensRow.y = 48;
37
+ ds.appendChild(tokensRow);
38
+ debug('Tokens row', { columns: tokensRow.children.length, height: tokensRow.height });
39
+
40
+ // Scanner names components after their file (capitalised, dashes kept): gradient-showcase.tsx → 'Gradient-showcase'
41
+ const EFFECTS_COMPONENTS = ['Gradient-showcase'];
42
+
43
+ const offset = tokensRow.y + tokensRow.height + 80;
44
+ const uiSection = createUIComponents(ds, {
45
+ themeNames,
46
+ yOffset: offset,
47
+ xOffset: 48,
48
+ excludeComponents: EFFECTS_COMPONENTS,
49
+ });
50
+
51
+ const effectsOffset = offset + (uiSection ? uiSection.height : 0) + 80;
52
+ createUIComponents(ds, {
53
+ themeNames,
54
+ yOffset: effectsOffset,
55
+ xOffset: 48,
56
+ sectionTitle: 'Effects',
57
+ onlyComponents: EFFECTS_COMPONENTS,
58
+ });
59
+ }
@@ -0,0 +1,173 @@
1
+ // --- Dev Server Integration ---
2
+ // Fetching is done via UI iframe (code.js sandbox has no network access).
3
+ // Architecture:
4
+ // 1. code.js shows UI (visible or hidden)
5
+ // 2. UI iframe loads and sends 'ui-ready'
6
+ // 3. code.js sends 'fetch-component-defs' request
7
+ // 4. UI fetches from localhost dev server
8
+ // 5. UI sends 'component-defs-result' back to code.js
9
+
10
+ import { debug } from './colors';
11
+
12
+ interface ComponentDefsResult {
13
+ requestId: string;
14
+ data: ComponentDefs | null;
15
+ }
16
+
17
+ interface ComponentDefs {
18
+ components: unknown[];
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ // Pending fetch callbacks keyed by requestId
23
+ let _pendingFetchCallbacks: Record<string, (data: ComponentDefs | null) => void> = {};
24
+ let _fetchRequestCounter = 0;
25
+ let _uiReady = false;
26
+ let _uiReadyCallbacks: Array<() => void> = [];
27
+
28
+ /**
29
+ * Wait for UI iframe to be ready (sends 'ui-ready' on load)
30
+ */
31
+ export function waitForUIReady(): Promise<void> {
32
+ if (_uiReady) return Promise.resolve();
33
+ return new Promise<void>(function (resolve) {
34
+ _uiReadyCallbacks.push(resolve);
35
+ // Safety timeout - if UI never sends ready, resolve anyway after 500ms
36
+ setTimeout(function () {
37
+ resolve();
38
+ }, 500);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Handle UI ready signal
44
+ */
45
+ export function handleUIReady(): void {
46
+ _uiReady = true;
47
+ for (let i = 0; i < _uiReadyCallbacks.length; i++) {
48
+ _uiReadyCallbacks[i]();
49
+ }
50
+ _uiReadyCallbacks = [];
51
+ }
52
+
53
+ /**
54
+ * Fetch component definitions from dev server via UI iframe.
55
+ * The UI iframe has network access; code.js sandbox does not.
56
+ * Returns a Promise that resolves with the data or null.
57
+ */
58
+ export async function fetchComponentDefsFromServer(): Promise<ComponentDefs | null> {
59
+ // Ensure UI is loaded and ready to receive messages
60
+ await waitForUIReady();
61
+
62
+ return new Promise<ComponentDefs | null>(function (resolve) {
63
+ const requestId = 'fetch-' + ++_fetchRequestCounter;
64
+ _pendingFetchCallbacks[requestId] = resolve;
65
+
66
+ debug('Sending fetch request to UI: ' + requestId);
67
+ figma.ui.postMessage({ type: 'fetch-component-defs', requestId: requestId });
68
+
69
+ // Timeout after 10s
70
+ setTimeout(function () {
71
+ if (_pendingFetchCallbacks[requestId]) {
72
+ delete _pendingFetchCallbacks[requestId];
73
+ debug('Fetch timed out');
74
+ resolve(null);
75
+ }
76
+ }, 10000);
77
+ });
78
+ }
79
+
80
+ // Pending image fetch callbacks keyed by requestId
81
+ type ImageFetchResult = { bytes: number[] | null; svgText: string | null };
82
+ const _pendingImageCallbacks: Record<string, (result: ImageFetchResult) => void> = {};
83
+ let _imageRequestCounter = 0;
84
+
85
+ /**
86
+ * Handle image fetch result from UI iframe
87
+ */
88
+ export function handleImageResult(msg: { requestId: string; bytes: number[] | null; svgText?: string | null }): void {
89
+ const cb = _pendingImageCallbacks[msg.requestId];
90
+ if (cb) {
91
+ delete _pendingImageCallbacks[msg.requestId];
92
+ cb({ bytes: msg.bytes, svgText: msg.svgText ?? null });
93
+ }
94
+ }
95
+
96
+ function fetchImageData(url: string): Promise<ImageFetchResult> {
97
+ return new Promise<ImageFetchResult>(function (resolve) {
98
+ const requestId = 'img-' + ++_imageRequestCounter;
99
+ _pendingImageCallbacks[requestId] = resolve;
100
+ figma.ui.postMessage({ type: 'fetch-image', url, requestId });
101
+ // Short timeout — CORS failures reject immediately; slow servers get 3s
102
+ setTimeout(function () {
103
+ if (_pendingImageCallbacks[requestId]) {
104
+ delete _pendingImageCallbacks[requestId];
105
+ resolve({ bytes: null, svgText: null });
106
+ }
107
+ }, 3000);
108
+ });
109
+ }
110
+
111
+ export type PrefetchResult = { hashMap: Map<string, string>; svgMap: Map<string, string> };
112
+
113
+ /**
114
+ * Pre-fetch a set of image srcs via the UI iframe (which has network access).
115
+ * Relative paths are resolved against localhost on the given port.
116
+ * Fetches all images in parallel. SVGs are stored as raw strings (for createNodeFromSvg);
117
+ * raster images are stored as figma imageHash fills.
118
+ */
119
+ export async function prefetchImages(
120
+ srcs: string[],
121
+ port: number | string
122
+ ): Promise<PrefetchResult> {
123
+ await waitForUIReady();
124
+ const hashMap = new Map<string, string>();
125
+ const svgMap = new Map<string, string>();
126
+ const unique = [...new Set(srcs.filter(Boolean))];
127
+
128
+ const entries = await Promise.all(
129
+ unique.map(async (src) => {
130
+ const url = src.startsWith('http') ? src : `http://localhost:${port}${src}`;
131
+ const data = await fetchImageData(url);
132
+ return { src, ...data };
133
+ })
134
+ );
135
+
136
+ for (const { src, bytes, svgText } of entries) {
137
+ if (svgText) {
138
+ svgMap.set(src, svgText);
139
+ } else if (bytes && bytes.length > 0) {
140
+ try {
141
+ const img = (figma as any).createImage(new Uint8Array(bytes));
142
+ hashMap.set(src, img.hash);
143
+ } catch (_e) { /* ignore */ }
144
+ }
145
+ }
146
+ return { hashMap, svgMap };
147
+ }
148
+
149
+ /**
150
+ * Handle fetch result from UI iframe
151
+ */
152
+ export function handleComponentDefsResult(msg: ComponentDefsResult): void {
153
+ const cb = _pendingFetchCallbacks[msg.requestId];
154
+ if (cb) {
155
+ delete _pendingFetchCallbacks[msg.requestId];
156
+ debug('Received component defs from UI, data: ' + (msg.data ? 'yes' : 'null'));
157
+ cb(msg.data);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Update COMPONENT_DEFS with fresh data from server.
163
+ * Returns { success: boolean, data: ComponentDefs | null } so the caller
164
+ * (main.ts) can assign the data to COMPONENT_DEFS.
165
+ */
166
+ export async function refreshComponentDefs(): Promise<{ success: boolean; data: ComponentDefs | null }> {
167
+ const freshDefs = await fetchComponentDefsFromServer();
168
+ if (freshDefs && freshDefs.components && freshDefs.components.length > 0) {
169
+ debug('Loaded ' + freshDefs.components.length + ' components from dev server');
170
+ return { success: true, data: freshDefs };
171
+ }
172
+ return { success: false, data: null };
173
+ }
@@ -0,0 +1,3 @@
1
+ /// <reference types="@figma/plugin-typings" />
2
+
3
+ declare const __html__: string;
@@ -0,0 +1,171 @@
1
+ declare const figma: any;
2
+
3
+ type FontStyleIndex = Record<string, string[]>;
4
+
5
+ let AVAILABLE_STYLES_BY_FAMILY: FontStyleIndex | null = null;
6
+ let INIT_PROMISE: Promise<void> | null = null;
7
+
8
+ function normalizeStyleToken(style: string): string {
9
+ return String(style || '')
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, ' ')
12
+ .trim();
13
+ }
14
+
15
+ function parseExplicitNumericWeight(style: string): number | null {
16
+ const normalized = normalizeStyleToken(style);
17
+ const match = normalized.match(/\b([1-9]00)\b/);
18
+ if (!match) return null;
19
+ const n = parseInt(match[1], 10);
20
+ if (!Number.isFinite(n)) return null;
21
+ return n;
22
+ }
23
+
24
+ export function inferFontWeight(style: string): number {
25
+ const explicit = parseExplicitNumericWeight(style);
26
+ if (explicit != null) return explicit;
27
+ const normalized = normalizeStyleToken(style);
28
+ if (!normalized) return 400;
29
+ if (/\bthin\b/.test(normalized)) return 100;
30
+ if (/\b(extra|ultra)\s*light\b/.test(normalized)) return 200;
31
+ if (/\blight\b/.test(normalized)) return 300;
32
+ if (/\b(book)\b/.test(normalized)) return 350;
33
+ if (/\b(normal|regular|roman)\b/.test(normalized)) return 400;
34
+ if (/\bmedium\b/.test(normalized)) return 500;
35
+ if (/\b(semi|demi)\s*bold\b/.test(normalized)) return 600;
36
+ if (/\bbold\b/.test(normalized) && !/\b(extra|ultra)\s*bold\b/.test(normalized)) return 700;
37
+ if (/\b(extra|ultra|heavy)\s*bold\b/.test(normalized) || /\bheavy\b/.test(normalized)) return 800;
38
+ if (/\bblack\b/.test(normalized)) return 900;
39
+ return 400;
40
+ }
41
+
42
+ function inferItalic(style: string): boolean {
43
+ const normalized = normalizeStyleToken(style);
44
+ return /\b(italic|oblique)\b/.test(normalized);
45
+ }
46
+
47
+ function uniquePush(target: string[], value: string): void {
48
+ for (let i = 0; i < target.length; i++) {
49
+ if (target[i] === value) return;
50
+ }
51
+ target.push(value);
52
+ }
53
+
54
+ export function rankStylesForRequest(availableStyles: string[], requestedStyle: string): string[] {
55
+ const ranked: Array<{ style: string; score: number; styleWeight: number }> = [];
56
+ const requestWeight = inferFontWeight(requestedStyle);
57
+ const requestItalic = inferItalic(requestedStyle);
58
+ const requestToken = normalizeStyleToken(requestedStyle);
59
+
60
+ for (let i = 0; i < availableStyles.length; i++) {
61
+ const style = availableStyles[i];
62
+ const styleWeight = inferFontWeight(style);
63
+ const styleItalic = inferItalic(style);
64
+ const styleToken = normalizeStyleToken(style);
65
+ let score = Math.abs(styleWeight - requestWeight);
66
+ if (styleItalic !== requestItalic) score += 120;
67
+ if (styleToken === requestToken) score -= 30;
68
+ if (styleToken.indexOf(requestToken) !== -1 && requestToken) score -= 10;
69
+ ranked.push({ style: style, score: score, styleWeight: styleWeight });
70
+ }
71
+
72
+ ranked.sort((a, b) => {
73
+ if (a.score !== b.score) return a.score - b.score;
74
+ if (a.styleWeight !== b.styleWeight) {
75
+ // For medium+ requests, prefer heavier fallback on exact distance ties.
76
+ // This avoids picking Regular over SemiBold for font-medium when 500 isn't available.
77
+ if (requestWeight >= 500) return b.styleWeight - a.styleWeight;
78
+ return a.styleWeight - b.styleWeight;
79
+ }
80
+ return a.style.localeCompare(b.style);
81
+ });
82
+
83
+ const out: string[] = [];
84
+ for (let i = 0; i < ranked.length; i++) {
85
+ uniquePush(out, ranked[i].style);
86
+ }
87
+ return out;
88
+ }
89
+
90
+ function buildFallbackCandidates(requestedStyle: string): string[] {
91
+ const normalized = normalizeStyleToken(requestedStyle);
92
+ if (normalized === 'medium') return ['Medium', 'SemiBold', 'Semibold', 'DemiBold', 'Regular'];
93
+ if (normalized === 'semibold' || normalized === 'semi bold' || normalized === 'demibold' || normalized === 'demi bold') {
94
+ return ['SemiBold', 'Semibold', 'DemiBold', 'Bold', 'Medium', 'Regular'];
95
+ }
96
+ if (normalized === 'bold') return ['Bold', 'SemiBold', 'Semibold', 'Medium', 'Regular'];
97
+ if (normalized === 'regular' || normalized === 'normal') return ['Regular', 'Book', 'Roman', 'Normal'];
98
+ const out: string[] = [];
99
+ if (requestedStyle) out.push(requestedStyle);
100
+ out.push('Regular');
101
+ return out;
102
+ }
103
+
104
+ function isWidthAxisVariant(style: string): boolean {
105
+ const normalized = normalizeStyleToken(style);
106
+ return /\b(condensed|narrow|compressed|expanded|extended)\b/.test(normalized);
107
+ }
108
+
109
+ function requestAsksForWidthAxis(requestedStyle: string): boolean {
110
+ return isWidthAxisVariant(requestedStyle);
111
+ }
112
+
113
+ export async function initializeFontStyleIndex(): Promise<void> {
114
+ if (AVAILABLE_STYLES_BY_FAMILY != null) return;
115
+ if (INIT_PROMISE) {
116
+ await INIT_PROMISE;
117
+ return;
118
+ }
119
+ INIT_PROMISE = (async () => {
120
+ const next: FontStyleIndex = {};
121
+ try {
122
+ const fonts = await figma.listAvailableFontsAsync();
123
+ for (let i = 0; i < fonts.length; i++) {
124
+ const entry = fonts[i];
125
+ const family = entry.fontName.family;
126
+ const style = entry.fontName.style;
127
+ if (!family || !style) continue;
128
+ if (!next[family]) next[family] = [];
129
+ uniquePush(next[family], style);
130
+ }
131
+ AVAILABLE_STYLES_BY_FAMILY = next;
132
+ } catch (_err) {
133
+ AVAILABLE_STYLES_BY_FAMILY = {};
134
+ }
135
+ })();
136
+ await INIT_PROMISE;
137
+ }
138
+
139
+ export function getKnownStylesForFamily(family: string): string[] {
140
+ if (AVAILABLE_STYLES_BY_FAMILY == null) return [];
141
+ const styles = AVAILABLE_STYLES_BY_FAMILY[family];
142
+ if (!styles) return [];
143
+ return styles.slice();
144
+ }
145
+
146
+ export function getFontStyleCandidatesForFamily(family: string, requestedStyle: string): string[] {
147
+ const out: string[] = [];
148
+ const styles = getKnownStylesForFamily(family);
149
+ if (styles.length > 0) {
150
+ if (requestedStyle) uniquePush(out, requestedStyle);
151
+ const wantsWidthAxis = requestAsksForWidthAxis(requestedStyle);
152
+ const preferredPool = wantsWidthAxis
153
+ ? styles
154
+ : styles.filter(style => !isWidthAxisVariant(style));
155
+ const rankedPreferred = rankStylesForRequest(preferredPool.length > 0 ? preferredPool : styles, requestedStyle);
156
+ for (let i = 0; i < rankedPreferred.length; i++) uniquePush(out, rankedPreferred[i]);
157
+ // Keep width-axis variants as late fallback when not explicitly requested.
158
+ if (!wantsWidthAxis) {
159
+ const rankedAll = rankStylesForRequest(styles, requestedStyle);
160
+ for (let i = 0; i < rankedAll.length; i++) uniquePush(out, rankedAll[i]);
161
+ }
162
+ // Keep generic aliases as a final fallback, never before ranked known styles.
163
+ const fallback = buildFallbackCandidates(requestedStyle);
164
+ for (let i = 0; i < fallback.length; i++) uniquePush(out, fallback[i]);
165
+ } else {
166
+ const fallback = buildFallbackCandidates(requestedStyle);
167
+ for (let i = 0; i < fallback.length; i++) uniquePush(out, fallback[i]);
168
+ }
169
+ if (out.length === 0) out.push('Regular');
170
+ return out;
171
+ }