inkbridge 0.1.0-beta.1
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.
- package/README.md +201 -0
- package/bin/inkhouse.mjs +171 -0
- package/code.js +11802 -0
- package/manifest.json +30 -0
- package/package.json +45 -0
- package/scanner/blob-placement-regression.ts +132 -0
- package/scanner/class-collector.ts +69 -0
- package/scanner/cli.ts +336 -0
- package/scanner/component-scanner.ts +2876 -0
- package/scanner/css-patch-regression.ts +112 -0
- package/scanner/css-token-reader-regression.ts +92 -0
- package/scanner/css-token-reader.ts +477 -0
- package/scanner/font-style-resolver-regression.ts +32 -0
- package/scanner/index.ts +9 -0
- package/scanner/radial-gradient-regression.ts +53 -0
- package/scanner/style-map.ts +149 -0
- package/scanner/tailwind-parser.ts +644 -0
- package/scanner/transform-math-regression.ts +42 -0
- package/scanner/types.ts +298 -0
- package/src/blob-placement.ts +111 -0
- package/src/change-detection.ts +204 -0
- package/src/class-utils.ts +105 -0
- package/src/clip-path-decorative.ts +194 -0
- package/src/color-resolver.ts +98 -0
- package/src/colors.ts +196 -0
- package/src/component-defs.ts +54 -0
- package/src/component-gen.ts +561 -0
- package/src/component-lookup.ts +82 -0
- package/src/config.ts +115 -0
- package/src/design-system.ts +59 -0
- package/src/dev-server.ts +173 -0
- package/src/figma-globals.d.ts +3 -0
- package/src/font-style-resolver.ts +171 -0
- package/src/github.ts +1465 -0
- package/src/icon-builder.ts +607 -0
- package/src/image-cache.ts +22 -0
- package/src/inline-text.ts +271 -0
- package/src/layout-parser.ts +667 -0
- package/src/layout-utils.ts +155 -0
- package/src/main.ts +687 -0
- package/src/node-ir.ts +595 -0
- package/src/pack-provider.ts +148 -0
- package/src/packs.ts +126 -0
- package/src/radial-gradient.ts +84 -0
- package/src/render-context.ts +138 -0
- package/src/responsive-analyzer.ts +139 -0
- package/src/state-analyzer.ts +143 -0
- package/src/story-builder.ts +1706 -0
- package/src/story-layout.ts +38 -0
- package/src/tailwind.ts +2379 -0
- package/src/text-builder.ts +116 -0
- package/src/text-line.ts +42 -0
- package/src/token-source.ts +43 -0
- package/src/tokens.ts +717 -0
- package/src/transform-math.ts +44 -0
- package/src/ui-builder.ts +1996 -0
- package/src/utility-resolver.ts +125 -0
- package/src/variables.ts +1042 -0
- package/src/width-solver.ts +466 -0
- package/templates/patch-tokens-route.ts +165 -0
- package/templates/scan-components-route.ts +57 -0
- package/ui.html +1222 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
// Tailwind Tokens Plugin - Entry Point
|
|
2
|
+
// Creates local color styles, a tokens preview page, and UI components.
|
|
3
|
+
// Includes "Push to Code" feature for GitHub PR creation.
|
|
4
|
+
// Universal plugin - configure for any project via Settings.
|
|
5
|
+
|
|
6
|
+
import { loadConfig, saveConfig, GITHUB_CONFIG } from './config';
|
|
7
|
+
import { TOKENS, COMPONENT_DEFS, applyScannedTokens, getCoreFontFamily, getThemeFontFamily, getThemeNames } from './tokens';
|
|
8
|
+
import { rebuildColorIndex } from './colors';
|
|
9
|
+
import { handleUIReady, handleImageResult, prefetchImages } from './dev-server';
|
|
10
|
+
import { setImageMap, setSvgMap } from './image-cache';
|
|
11
|
+
import { applyPack, type Pack } from './packs';
|
|
12
|
+
import { handlePackResult, refreshPack } from './pack-provider';
|
|
13
|
+
import { createOrUpdateStyles } from './variables';
|
|
14
|
+
import { tailwindForNode } from './tailwind';
|
|
15
|
+
import { detectComponentChanges } from './change-detection';
|
|
16
|
+
import { pushToGitHub, pushToGitHubWithComponents, handleGitHubFetchResult, handlePatchCssResult, previewTokenChanges } from './github';
|
|
17
|
+
import { buildDesignSystemSinglePage } from './design-system';
|
|
18
|
+
import { waitForAllFonts } from './text-builder';
|
|
19
|
+
import { getKnownStylesForFamily, initializeFontStyleIndex } from './font-style-resolver';
|
|
20
|
+
import type { ResolvedTokenSourceMode, ScannedTokenMap, TokenSourceMode } from './token-source';
|
|
21
|
+
|
|
22
|
+
const PACK_LOAD_ERROR = 'Dev server not reachable / pack not found.';
|
|
23
|
+
const TOKEN_SOURCE_INFO_KEY = 'token_source_info';
|
|
24
|
+
|
|
25
|
+
type TokenSourceInfo = {
|
|
26
|
+
source: string;
|
|
27
|
+
mode: ResolvedTokenSourceMode;
|
|
28
|
+
requestedMode?: TokenSourceMode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let LAST_TOKEN_SOURCE_INFO: TokenSourceInfo | null = null;
|
|
32
|
+
|
|
33
|
+
function getPackLoadErrorMessage(error?: string): string {
|
|
34
|
+
if (error === 'incompatible-schema-version' || error === 'incompatible-pack-version') {
|
|
35
|
+
return 'Incompatible scanner contract. Update plugin and scanner to matching versions.';
|
|
36
|
+
}
|
|
37
|
+
if (error === 'invalid-pack') {
|
|
38
|
+
return 'Invalid pack payload from dev server.';
|
|
39
|
+
}
|
|
40
|
+
return PACK_LOAD_ERROR;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isOfflineError(error?: string): boolean {
|
|
44
|
+
return !error || error === 'timeout' || error === 'no-result';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function coerceTokenSourceInfo(raw: any): TokenSourceInfo {
|
|
48
|
+
const mode: ResolvedTokenSourceMode =
|
|
49
|
+
raw && (raw.mode === 'css' || raw.mode === 'dtcg' || raw.mode === 'embedded')
|
|
50
|
+
? raw.mode
|
|
51
|
+
: 'embedded';
|
|
52
|
+
const requestedMode: TokenSourceMode | undefined =
|
|
53
|
+
raw && (raw.requestedMode === 'auto' || raw.requestedMode === 'css' || raw.requestedMode === 'dtcg')
|
|
54
|
+
? raw.requestedMode
|
|
55
|
+
: undefined;
|
|
56
|
+
const source = raw && typeof raw.source === 'string' && raw.source.trim()
|
|
57
|
+
? raw.source.trim()
|
|
58
|
+
: 'embedded:tokens.ts';
|
|
59
|
+
return { source, mode, requestedMode };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getTokenSourceInfoFromPack(pack: Pack | null): TokenSourceInfo {
|
|
63
|
+
const raw = (pack && pack.tokens) ? (pack.tokens as ScannedTokenMap) : null;
|
|
64
|
+
if (!raw) return { source: 'embedded:tokens.ts', mode: 'embedded' };
|
|
65
|
+
return coerceTokenSourceInfo(raw);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function loadTokenSourceInfoFromStorage(): Promise<TokenSourceInfo> {
|
|
69
|
+
if (LAST_TOKEN_SOURCE_INFO) return LAST_TOKEN_SOURCE_INFO;
|
|
70
|
+
const stored = await figma.clientStorage.getAsync(TOKEN_SOURCE_INFO_KEY);
|
|
71
|
+
LAST_TOKEN_SOURCE_INFO = coerceTokenSourceInfo(stored);
|
|
72
|
+
return LAST_TOKEN_SOURCE_INFO;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function saveAndBroadcastTokenSourceInfo(info: TokenSourceInfo): Promise<void> {
|
|
76
|
+
LAST_TOKEN_SOURCE_INFO = info;
|
|
77
|
+
await figma.clientStorage.setAsync(TOKEN_SOURCE_INFO_KEY, info);
|
|
78
|
+
figma.ui.postMessage({ type: 'token-source-info', info: info });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function postTokenSourceInfoToUI(): Promise<void> {
|
|
82
|
+
const info = await loadTokenSourceInfoFromStorage();
|
|
83
|
+
figma.ui.postMessage({ type: 'token-source-info', info: info });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Image src collection
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function collectImageSrcsFromJsxTree(node: any, out: Set<string>): void {
|
|
91
|
+
if (!node || typeof node !== 'object') return;
|
|
92
|
+
const tag = node.tagName;
|
|
93
|
+
if (tag === 'img' || tag === 'Image') {
|
|
94
|
+
const src = node.props?.src;
|
|
95
|
+
if (typeof src === 'string' && src.length > 0 && src !== 'src') {
|
|
96
|
+
out.add(src);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(node.children)) {
|
|
100
|
+
for (const child of node.children) collectImageSrcsFromJsxTree(child, out);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function collectImageSrcs(components: any[]): string[] {
|
|
105
|
+
const srcs = new Set<string>();
|
|
106
|
+
for (const comp of components) {
|
|
107
|
+
const analysis = comp?.analysis;
|
|
108
|
+
if (!analysis) continue;
|
|
109
|
+
collectImageSrcsFromJsxTree(analysis.jsxTree, srcs);
|
|
110
|
+
for (const story of analysis.stories ?? []) {
|
|
111
|
+
collectImageSrcsFromJsxTree(story.jsxTree, srcs);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...srcs];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ----------------------- License Checking -----------------------
|
|
118
|
+
|
|
119
|
+
let pendingLicenseResolve: ((tier: 'free' | 'pro') => void) | null = null;
|
|
120
|
+
let licenseValidatingFromSettings = false;
|
|
121
|
+
|
|
122
|
+
async function checkLicenseOnline(key: string): Promise<'free' | 'pro'> {
|
|
123
|
+
return new Promise<'free' | 'pro'>(function(resolve) {
|
|
124
|
+
pendingLicenseResolve = resolve;
|
|
125
|
+
figma.ui.postMessage({ type: 'validate-license', key: key });
|
|
126
|
+
setTimeout(function() {
|
|
127
|
+
if (pendingLicenseResolve === resolve) {
|
|
128
|
+
pendingLicenseResolve = null;
|
|
129
|
+
resolve('free');
|
|
130
|
+
}
|
|
131
|
+
}, 5000);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function checkLicense(): Promise<'free' | 'pro'> {
|
|
136
|
+
const key = await figma.clientStorage.getAsync('license_key');
|
|
137
|
+
if (!key) return 'free';
|
|
138
|
+
|
|
139
|
+
const cachedTier = await figma.clientStorage.getAsync('license_tier') as string | null;
|
|
140
|
+
const cachedAt = await figma.clientStorage.getAsync('license_cache_at') as string | null;
|
|
141
|
+
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
|
|
142
|
+
|
|
143
|
+
// Only trust cached 'pro' — never trust cached 'free' (avoids stale negatives)
|
|
144
|
+
if (cachedTier === 'pro' && cachedAt && (Date.now() - Number(cachedAt)) < SEVEN_DAYS) {
|
|
145
|
+
return 'pro';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return checkLicenseOnline(key as string);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ----------------------- Generate Command -----------------------
|
|
152
|
+
|
|
153
|
+
async function warmThemeFonts(): Promise<string[]> {
|
|
154
|
+
const baseFont = getCoreFontFamily(TOKENS) || 'Inter';
|
|
155
|
+
const themeNames = getThemeNames(TOKENS);
|
|
156
|
+
const fontsSet = new Set<string>();
|
|
157
|
+
fontsSet.add(baseFont);
|
|
158
|
+
fontsSet.add('Inter');
|
|
159
|
+
|
|
160
|
+
for (let i = 0; i < themeNames.length; i++) {
|
|
161
|
+
const theme = themeNames[i];
|
|
162
|
+
const sansFont = getThemeFontFamily(TOKENS, theme, 'sans') || baseFont;
|
|
163
|
+
const headingFont = getThemeFontFamily(TOKENS, theme, 'heading') || sansFont || baseFont;
|
|
164
|
+
if (sansFont) fontsSet.add(sansFont);
|
|
165
|
+
if (headingFont) fontsSet.add(headingFont);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const fontsToLoad = Array.from(fontsSet);
|
|
169
|
+
const failedFonts: string[] = [];
|
|
170
|
+
await initializeFontStyleIndex();
|
|
171
|
+
|
|
172
|
+
const fallbackStyles = ['Regular', 'Medium', 'SemiBold', 'Bold'];
|
|
173
|
+
for (const family of fontsToLoad) {
|
|
174
|
+
const knownStyles = getKnownStylesForFamily(family);
|
|
175
|
+
const stylesToTry = knownStyles.length > 0 ? knownStyles : fallbackStyles;
|
|
176
|
+
let loadedAny = false;
|
|
177
|
+
for (const style of stylesToTry) {
|
|
178
|
+
const ok = await figma.loadFontAsync({ family, style }).then(() => true).catch(() => false);
|
|
179
|
+
if (ok) loadedAny = true;
|
|
180
|
+
}
|
|
181
|
+
if (!loadedAny && family !== 'Inter') failedFonts.push(family);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return failedFonts;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function runGenerate(): Promise<void> {
|
|
188
|
+
let shouldClose = true;
|
|
189
|
+
try {
|
|
190
|
+
await loadConfig();
|
|
191
|
+
// Show UI (hidden) so it can relay fetch requests
|
|
192
|
+
// code.js sandbox has no network access - UI iframe does the fetching
|
|
193
|
+
figma.showUI(__html__, { visible: false, width: 1, height: 1 });
|
|
194
|
+
|
|
195
|
+
figma.notify('🔄 Loading pack...');
|
|
196
|
+
const result = await refreshPack(GITHUB_CONFIG || undefined);
|
|
197
|
+
|
|
198
|
+
if (!result.success || !result.pack) {
|
|
199
|
+
const message = getPackLoadErrorMessage(result.error);
|
|
200
|
+
figma.notify('❌ ' + message);
|
|
201
|
+
figma.showUI(__html__, { width: 360, height: 400 });
|
|
202
|
+
if (isOfflineError(result.error)) {
|
|
203
|
+
figma.ui.postMessage({ type: 'show-view', view: 'offline' });
|
|
204
|
+
} else {
|
|
205
|
+
await loadConfig();
|
|
206
|
+
figma.ui.postMessage({ type: 'show-view', view: 'settings' });
|
|
207
|
+
const hasToken = await figma.clientStorage.getAsync('github_token');
|
|
208
|
+
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!hasToken });
|
|
209
|
+
await postTokenSourceInfoToUI();
|
|
210
|
+
figma.ui.postMessage({ type: 'pack-load-error', message: message });
|
|
211
|
+
}
|
|
212
|
+
shouldClose = false;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
applyPack(result.pack);
|
|
217
|
+
applyScannedTokens(result.pack.tokens);
|
|
218
|
+
await saveAndBroadcastTokenSourceInfo(getTokenSourceInfoFromPack(result.pack));
|
|
219
|
+
|
|
220
|
+
const failedFonts = await warmThemeFonts();
|
|
221
|
+
if (failedFonts.length) {
|
|
222
|
+
figma.notify(`⚠️ Font(s) not found in Figma: ${failedFonts.join(', ')}. Enable them via Figma's Google Fonts panel or install locally. Falling back to Inter.`, { timeout: 6000 });
|
|
223
|
+
}
|
|
224
|
+
figma.notify('✅ Loaded ' + COMPONENT_DEFS.components.length + ' components');
|
|
225
|
+
|
|
226
|
+
// Pre-fetch all image srcs found in component definitions
|
|
227
|
+
const imageSrcs = collectImageSrcs(COMPONENT_DEFS.components as any[]);
|
|
228
|
+
if (imageSrcs.length > 0) {
|
|
229
|
+
// Extract port from the source URL (e.g. 'http://localhost:3000/api/...')
|
|
230
|
+
const portMatch = result.source?.match(/localhost:(\d+)/);
|
|
231
|
+
const devPort = portMatch ? parseInt(portMatch[1], 10) : 3000;
|
|
232
|
+
const { hashMap, svgMap } = await prefetchImages(imageSrcs, devPort);
|
|
233
|
+
setImageMap(hashMap);
|
|
234
|
+
setSvgMap(svgMap);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
rebuildColorIndex(TOKENS);
|
|
238
|
+
createOrUpdateStyles();
|
|
239
|
+
|
|
240
|
+
// Single page build to avoid page-limit errors on free plans
|
|
241
|
+
buildDesignSystemSinglePage();
|
|
242
|
+
figma.notify('✅ Built Design System page (tokens + pack)');
|
|
243
|
+
} catch (e: any) {
|
|
244
|
+
figma.notify('❌ Failed: ' + e.message);
|
|
245
|
+
} finally {
|
|
246
|
+
if (shouldClose) {
|
|
247
|
+
await waitForAllFonts();
|
|
248
|
+
figma.closePlugin();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ----------------------- Menu Command Handling -----------------------
|
|
254
|
+
|
|
255
|
+
function debugSelection(): void {
|
|
256
|
+
const selection = figma.currentPage.selection || [];
|
|
257
|
+
if (selection.length === 0) {
|
|
258
|
+
figma.notify('No selection. Select a node and run Debug Selection.');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const payload: any[] = [];
|
|
263
|
+
for (const node of selection) {
|
|
264
|
+
const entry: any = {
|
|
265
|
+
id: node.id,
|
|
266
|
+
name: node.name,
|
|
267
|
+
type: node.type,
|
|
268
|
+
width: (node as any).width,
|
|
269
|
+
height: (node as any).height,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if ('layoutMode' in node) {
|
|
273
|
+
entry.layoutMode = (node as any).layoutMode;
|
|
274
|
+
entry.layoutWrap = (node as any).layoutWrap;
|
|
275
|
+
entry.itemSpacing = (node as any).itemSpacing;
|
|
276
|
+
entry.counterAxisSpacing = (node as any).counterAxisSpacing;
|
|
277
|
+
entry.primaryAxisSizingMode = (node as any).primaryAxisSizingMode;
|
|
278
|
+
entry.counterAxisSizingMode = (node as any).counterAxisSizingMode;
|
|
279
|
+
entry.primaryAxisAlignItems = (node as any).primaryAxisAlignItems;
|
|
280
|
+
entry.counterAxisAlignItems = (node as any).counterAxisAlignItems;
|
|
281
|
+
entry.padding = {
|
|
282
|
+
top: (node as any).paddingTop,
|
|
283
|
+
right: (node as any).paddingRight,
|
|
284
|
+
bottom: (node as any).paddingBottom,
|
|
285
|
+
left: (node as any).paddingLeft,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if ('layoutAlign' in node) {
|
|
290
|
+
entry.layoutAlign = (node as any).layoutAlign;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if ('layoutGrow' in node) {
|
|
294
|
+
entry.layoutGrow = (node as any).layoutGrow;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if ('layoutPositioning' in node) {
|
|
298
|
+
entry.layoutPositioning = (node as any).layoutPositioning;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if ('children' in node) {
|
|
302
|
+
const childSummary: any[] = [];
|
|
303
|
+
const children = (node as any).children || [];
|
|
304
|
+
for (let i = 0; i < children.length; i++) {
|
|
305
|
+
const child = children[i];
|
|
306
|
+
childSummary.push({
|
|
307
|
+
name: child.name,
|
|
308
|
+
type: child.type,
|
|
309
|
+
width: child.width,
|
|
310
|
+
height: child.height,
|
|
311
|
+
layoutGrow: child.layoutGrow,
|
|
312
|
+
layoutAlign: child.layoutAlign,
|
|
313
|
+
layoutPositioning: child.layoutPositioning,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
entry.children = childSummary;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
payload.push(entry);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
console.log('[TailwindTokens][DebugSelection]', payload);
|
|
323
|
+
figma.notify('Selection logged to console.');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function handleCommand(command: string): Promise<void> {
|
|
327
|
+
switch (command) {
|
|
328
|
+
case 'push': {
|
|
329
|
+
// Show hidden UI so it can relay the license validation fetch
|
|
330
|
+
figma.showUI(__html__, { visible: false, width: 1, height: 1 });
|
|
331
|
+
const tier = await checkLicense();
|
|
332
|
+
if (tier !== 'pro') {
|
|
333
|
+
figma.showUI(__html__, { visible: true, width: 320, height: 160 });
|
|
334
|
+
figma.ui.postMessage({ type: 'show-view', view: 'upgrade' });
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
figma.showUI(__html__, { visible: true, width: 360, height: 520 });
|
|
338
|
+
figma.ui.postMessage({ type: 'show-view', view: 'sync' });
|
|
339
|
+
figma.ui.postMessage({ type: 'changes-detected', changes: { tokens: false, components: [] } });
|
|
340
|
+
figma.ui.postMessage({ type: 'sync-status', message: 'Detecting changes...', success: null });
|
|
341
|
+
figma.ui.postMessage({ type: 'auto-start-detect' });
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
case 'configure': {
|
|
346
|
+
figma.showUI(__html__, { width: 320, height: 340 });
|
|
347
|
+
figma.ui.postMessage({ type: 'show-view', view: 'configure' });
|
|
348
|
+
const existingToken = await figma.clientStorage.getAsync('github_token');
|
|
349
|
+
figma.ui.postMessage({ type: 'token-loaded', hasToken: !!existingToken });
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
case 'settings': {
|
|
354
|
+
await loadConfig();
|
|
355
|
+
const tokenExists = await figma.clientStorage.getAsync('github_token');
|
|
356
|
+
const licenseKeyExists = await figma.clientStorage.getAsync('license_key');
|
|
357
|
+
const cachedSettingsTier = await figma.clientStorage.getAsync('license_tier') as string | null;
|
|
358
|
+
figma.showUI(__html__, { width: 320, height: 400 });
|
|
359
|
+
figma.ui.postMessage({ type: 'show-view', view: 'settings' });
|
|
360
|
+
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!tokenExists });
|
|
361
|
+
figma.ui.postMessage({ type: 'license-key-loaded', hasKey: !!licenseKeyExists, tier: cachedSettingsTier || 'free' });
|
|
362
|
+
await postTokenSourceInfoToUI();
|
|
363
|
+
// Re-validate in background so badge is always accurate
|
|
364
|
+
if (licenseKeyExists) {
|
|
365
|
+
figma.ui.postMessage({ type: 'validate-license', key: licenseKeyExists as string });
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
case 'debug-selection': {
|
|
371
|
+
debugSelection();
|
|
372
|
+
figma.closePlugin();
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
case 'generate':
|
|
377
|
+
default:
|
|
378
|
+
await runGenerate();
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ----------------------- UI Message Handler -----------------------
|
|
384
|
+
|
|
385
|
+
figma.ui.onmessage = async (msg: any) => {
|
|
386
|
+
// Handle UI iframe ready signal
|
|
387
|
+
if (msg.type === 'ui-ready') {
|
|
388
|
+
handleUIReady();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Handle license validation result from UI iframe
|
|
393
|
+
if (msg.type === 'license-result') {
|
|
394
|
+
const tier: 'free' | 'pro' = msg.tier === 'pro' ? 'pro' : 'free';
|
|
395
|
+
await figma.clientStorage.setAsync('license_tier', tier);
|
|
396
|
+
await figma.clientStorage.setAsync('license_cache_at', String(Date.now()));
|
|
397
|
+
if (pendingLicenseResolve) {
|
|
398
|
+
pendingLicenseResolve(tier);
|
|
399
|
+
pendingLicenseResolve = null;
|
|
400
|
+
}
|
|
401
|
+
if (licenseValidatingFromSettings) {
|
|
402
|
+
licenseValidatingFromSettings = false;
|
|
403
|
+
const statusMsg = tier === 'pro' ? 'Pro license activated! Redirecting...' : 'Invalid license key.';
|
|
404
|
+
figma.ui.postMessage({ type: 'settings-status', message: statusMsg, success: tier === 'pro' });
|
|
405
|
+
if (tier === 'pro') {
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
figma.ui.postMessage({ type: 'show-view', view: 'overview' });
|
|
408
|
+
}, 1200);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Always push updated badge to settings (harmless if settings isn't open)
|
|
412
|
+
const currentKey = await figma.clientStorage.getAsync('license_key');
|
|
413
|
+
figma.ui.postMessage({ type: 'license-key-loaded', hasKey: !!currentKey, tier: tier });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Handle pack fetch result from UI iframe
|
|
418
|
+
if (msg.type === 'pack-result') {
|
|
419
|
+
handlePackResult(msg);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Handle image fetch result from UI iframe
|
|
424
|
+
if (msg.type === 'image-result') {
|
|
425
|
+
handleImageResult(msg);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Handle GitHub API fetch result from UI iframe
|
|
430
|
+
if (msg.type === 'github-fetch-result') {
|
|
431
|
+
handleGitHubFetchResult(msg);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (msg.type === 'patch-css-result') {
|
|
436
|
+
handlePatchCssResult(msg);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (msg.type === 'save-github-token') {
|
|
441
|
+
try {
|
|
442
|
+
await figma.clientStorage.setAsync('github_token', msg.token);
|
|
443
|
+
figma.ui.postMessage({ type: 'config-status', message: 'Token saved successfully!', success: true });
|
|
444
|
+
setTimeout(() => {
|
|
445
|
+
figma.ui.postMessage({ type: 'show-view', view: 'overview' });
|
|
446
|
+
}, 1000);
|
|
447
|
+
} catch (e: any) {
|
|
448
|
+
figma.ui.postMessage({ type: 'config-status', message: 'Failed to save token: ' + e.message, success: false });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (msg.type === 'save-settings') {
|
|
453
|
+
try {
|
|
454
|
+
const newConfig = {
|
|
455
|
+
owner: msg.owner || '',
|
|
456
|
+
repo: msg.repo || '',
|
|
457
|
+
baseBranch: msg.baseBranch || 'main',
|
|
458
|
+
tokenPath: msg.tokenPath || 'design-tokens/tokens.dtcg.json',
|
|
459
|
+
tokenSourceMode: msg.tokenSourceMode || 'auto',
|
|
460
|
+
cssTokenPath: msg.cssTokenPath || '',
|
|
461
|
+
syncDtcgOnPush: msg.syncDtcgOnPush === true,
|
|
462
|
+
allowNewTokensFromFigma: msg.allowNewTokensFromFigma === true,
|
|
463
|
+
newTokenPrefixes: Array.isArray(msg.newTokenPrefixes) ? msg.newTokenPrefixes : [],
|
|
464
|
+
projectName: msg.projectName || '',
|
|
465
|
+
};
|
|
466
|
+
await saveConfig(newConfig);
|
|
467
|
+
|
|
468
|
+
if (msg.token) {
|
|
469
|
+
await figma.clientStorage.setAsync('github_token', msg.token);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (msg.licenseKey) {
|
|
473
|
+
await figma.clientStorage.setAsync('license_key', msg.licenseKey);
|
|
474
|
+
await figma.clientStorage.setAsync('license_tier', '');
|
|
475
|
+
await figma.clientStorage.setAsync('license_cache_at', '');
|
|
476
|
+
licenseValidatingFromSettings = true;
|
|
477
|
+
figma.ui.postMessage({ type: 'settings-status', message: 'Validating license key...', success: null });
|
|
478
|
+
figma.ui.postMessage({ type: 'validate-license', key: msg.licenseKey });
|
|
479
|
+
} else {
|
|
480
|
+
figma.ui.postMessage({ type: 'settings-status', message: 'Settings saved!', success: true });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// After saving settings (without license key change), go back to overview
|
|
484
|
+
if (!licenseValidatingFromSettings) {
|
|
485
|
+
setTimeout(() => {
|
|
486
|
+
figma.ui.postMessage({ type: 'show-view', view: 'overview' });
|
|
487
|
+
}, 800);
|
|
488
|
+
}
|
|
489
|
+
} catch (e: any) {
|
|
490
|
+
figma.ui.postMessage({ type: 'settings-status', message: 'Failed: ' + e.message, success: false });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (msg.type === 'push-to-github') {
|
|
495
|
+
try {
|
|
496
|
+
figma.ui.postMessage({ type: 'push-status', message: 'Creating branch...', success: null });
|
|
497
|
+
const prUrl = await pushToGitHub(msg.commitMessage, msg.prDescription);
|
|
498
|
+
figma.ui.postMessage({ type: 'push-status', message: 'PR created!', success: true, url: prUrl });
|
|
499
|
+
} catch (e: any) {
|
|
500
|
+
figma.ui.postMessage({ type: 'push-status', message: 'Failed: ' + e.message, success: false });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (msg.type === 'detect-changes') {
|
|
505
|
+
try {
|
|
506
|
+
await loadConfig();
|
|
507
|
+
const pushToken = await figma.clientStorage.getAsync('github_token') as string | null;
|
|
508
|
+
if (!pushToken) {
|
|
509
|
+
figma.ui.postMessage({ type: 'show-view', view: 'settings' });
|
|
510
|
+
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: false });
|
|
511
|
+
await postTokenSourceInfoToUI();
|
|
512
|
+
figma.ui.postMessage({ type: 'pack-load-error', message: 'GitHub token missing. Add it in Project Settings.' });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const result = await refreshPack(GITHUB_CONFIG || undefined);
|
|
517
|
+
if (!result.success || !result.pack) {
|
|
518
|
+
const message = getPackLoadErrorMessage(result.error);
|
|
519
|
+
if (isOfflineError(result.error)) {
|
|
520
|
+
figma.ui.postMessage({ type: 'show-view', view: 'offline' });
|
|
521
|
+
} else {
|
|
522
|
+
figma.ui.postMessage({ type: 'show-view', view: 'settings' });
|
|
523
|
+
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!pushToken });
|
|
524
|
+
await postTokenSourceInfoToUI();
|
|
525
|
+
figma.ui.postMessage({ type: 'pack-load-error', message: message });
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
applyPack(result.pack);
|
|
530
|
+
applyScannedTokens(result.pack.tokens);
|
|
531
|
+
await saveAndBroadcastTokenSourceInfo(getTokenSourceInfoFromPack(result.pack));
|
|
532
|
+
|
|
533
|
+
// Detect changes between Figma and code
|
|
534
|
+
const changes = detectComponentChanges();
|
|
535
|
+
if ((changes as any).error) {
|
|
536
|
+
figma.showUI(__html__, { width: 360, height: 520 });
|
|
537
|
+
figma.ui.postMessage({ type: 'show-view', view: 'sync' });
|
|
538
|
+
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!pushToken });
|
|
539
|
+
await postTokenSourceInfoToUI();
|
|
540
|
+
figma.ui.postMessage({ type: 'changes-detected', changes: { tokens: false, components: [] } });
|
|
541
|
+
figma.ui.postMessage({ type: 'sync-status', message: String((changes as any).error), success: false });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const componentChanges = Array.isArray((changes as any).changes) ? (changes as any).changes : [];
|
|
545
|
+
const tokenPreview = await previewTokenChanges(pushToken);
|
|
546
|
+
const hasTokenChanges = tokenPreview.hasChanges;
|
|
547
|
+
|
|
548
|
+
if (!hasTokenChanges && componentChanges.length === 0) {
|
|
549
|
+
figma.showUI(__html__, { width: 360, height: 520 });
|
|
550
|
+
figma.ui.postMessage({ type: 'show-view', view: 'sync' });
|
|
551
|
+
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!pushToken });
|
|
552
|
+
await postTokenSourceInfoToUI();
|
|
553
|
+
figma.ui.postMessage({ type: 'changes-detected', changes: { tokens: false, components: [] } });
|
|
554
|
+
figma.ui.postMessage({
|
|
555
|
+
type: 'sync-status',
|
|
556
|
+
message: 'All tokens and components are already in sync. Nothing to commit.',
|
|
557
|
+
success: true,
|
|
558
|
+
});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Show sync view with detected changes
|
|
563
|
+
figma.showUI(__html__, { width: 360, height: 520 });
|
|
564
|
+
figma.ui.postMessage({ type: 'show-view', view: 'sync' });
|
|
565
|
+
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!pushToken });
|
|
566
|
+
await postTokenSourceInfoToUI();
|
|
567
|
+
figma.ui.postMessage({ type: 'changes-detected', changes: { tokens: hasTokenChanges, components: componentChanges } });
|
|
568
|
+
} catch (e: any) {
|
|
569
|
+
figma.showUI(__html__, { width: 360, height: 520 });
|
|
570
|
+
figma.ui.postMessage({ type: 'show-view', view: 'sync' });
|
|
571
|
+
figma.ui.postMessage({ type: 'sync-status', message: 'Failed: ' + e.message, success: false });
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (msg.type === 'sync-to-github') {
|
|
576
|
+
try {
|
|
577
|
+
figma.notify('Sync request received', { timeout: 1200 });
|
|
578
|
+
figma.ui.postMessage({ type: 'sync-status', message: 'Starting sync...', success: null });
|
|
579
|
+
const prUrl = await pushToGitHubWithComponents(
|
|
580
|
+
msg.commitMessage,
|
|
581
|
+
msg.prDescription,
|
|
582
|
+
msg.includeTokens,
|
|
583
|
+
msg.components || [],
|
|
584
|
+
(message: string) => {
|
|
585
|
+
figma.ui.postMessage({ type: 'sync-status', message, success: null });
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
figma.ui.postMessage({ type: 'sync-status', message: 'PR created!', success: true, url: prUrl });
|
|
589
|
+
} catch (e: any) {
|
|
590
|
+
figma.notify('Sync failed: ' + e.message, { timeout: 2500 });
|
|
591
|
+
figma.ui.postMessage({ type: 'sync-status', message: 'Failed: ' + e.message, success: false });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (msg.type === 'show-push') {
|
|
596
|
+
await loadConfig();
|
|
597
|
+
const hasPushToken = await figma.clientStorage.getAsync('github_token');
|
|
598
|
+
figma.ui.postMessage({ type: 'show-view', view: 'push' });
|
|
599
|
+
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!hasPushToken });
|
|
600
|
+
await postTokenSourceInfoToUI();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (msg.type === 'show-overview') {
|
|
604
|
+
figma.ui.postMessage({ type: 'show-view', view: 'overview' });
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (msg.type === 'retry-pack-load') {
|
|
608
|
+
figma.notify('🔄 Retrying...');
|
|
609
|
+
await loadConfig();
|
|
610
|
+
const retryResult = await refreshPack(GITHUB_CONFIG || undefined);
|
|
611
|
+
if (!retryResult.success || !retryResult.pack) {
|
|
612
|
+
const retryMessage = getPackLoadErrorMessage(retryResult.error);
|
|
613
|
+
figma.notify('❌ ' + retryMessage);
|
|
614
|
+
if (isOfflineError(retryResult.error)) {
|
|
615
|
+
figma.ui.postMessage({ type: 'show-view', view: 'offline' });
|
|
616
|
+
} else {
|
|
617
|
+
await loadConfig();
|
|
618
|
+
figma.ui.postMessage({ type: 'show-view', view: 'settings' });
|
|
619
|
+
const hasToken = await figma.clientStorage.getAsync('github_token');
|
|
620
|
+
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!hasToken });
|
|
621
|
+
await postTokenSourceInfoToUI();
|
|
622
|
+
figma.ui.postMessage({ type: 'pack-load-error', message: retryMessage });
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
applyPack(retryResult.pack);
|
|
627
|
+
applyScannedTokens(retryResult.pack.tokens);
|
|
628
|
+
await saveAndBroadcastTokenSourceInfo(getTokenSourceInfoFromPack(retryResult.pack));
|
|
629
|
+
const failedFonts = await warmThemeFonts();
|
|
630
|
+
if (failedFonts.length) {
|
|
631
|
+
figma.notify(`⚠️ Font(s) not found in Figma: ${failedFonts.join(', ')}. Falling back to Inter.`, { timeout: 6000 });
|
|
632
|
+
}
|
|
633
|
+
rebuildColorIndex(TOKENS);
|
|
634
|
+
createOrUpdateStyles();
|
|
635
|
+
buildDesignSystemSinglePage();
|
|
636
|
+
figma.notify('✅ Built Design System page');
|
|
637
|
+
await waitForAllFonts();
|
|
638
|
+
figma.closePlugin();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (msg.type === 'resize') {
|
|
642
|
+
const h = typeof msg.height === 'number' ? Math.max(80, msg.height) : 400;
|
|
643
|
+
figma.ui.resize(320, h);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (msg.type === 'show-settings') {
|
|
647
|
+
await loadConfig();
|
|
648
|
+
const tokenExists = await figma.clientStorage.getAsync('github_token');
|
|
649
|
+
const licenseKeyExists = await figma.clientStorage.getAsync('license_key');
|
|
650
|
+
const cachedMsgTier = await figma.clientStorage.getAsync('license_tier') as string | null;
|
|
651
|
+
figma.ui.postMessage({ type: 'show-view', view: 'settings' });
|
|
652
|
+
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!tokenExists });
|
|
653
|
+
figma.ui.postMessage({ type: 'license-key-loaded', hasKey: !!licenseKeyExists, tier: cachedMsgTier || 'free' });
|
|
654
|
+
await postTokenSourceInfoToUI();
|
|
655
|
+
// Re-validate in background so badge is always accurate
|
|
656
|
+
if (licenseKeyExists) {
|
|
657
|
+
figma.ui.postMessage({ type: 'validate-license', key: licenseKeyExists as string });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
// ----------------------- Dev Mode Codegen -----------------------
|
|
663
|
+
|
|
664
|
+
if (figma.codegen && typeof figma.codegen.on === 'function') {
|
|
665
|
+
// Initialize color index for codegen
|
|
666
|
+
rebuildColorIndex(TOKENS);
|
|
667
|
+
|
|
668
|
+
figma.codegen.on('generate', (event: any) => {
|
|
669
|
+
try {
|
|
670
|
+
const node = event.node;
|
|
671
|
+
const classes = tailwindForNode(node);
|
|
672
|
+
const code = 'className="' + classes + '"';
|
|
673
|
+
return [{ title: 'Tailwind CSS', code, language: 'PLAINTEXT' }];
|
|
674
|
+
} catch (e: any) {
|
|
675
|
+
return [{ title: 'Tailwind CSS', code: '// Unable to generate: ' + e.message, language: 'PLAINTEXT' }];
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ----------------------- Main Entry Point -----------------------
|
|
681
|
+
|
|
682
|
+
if (figma.command) {
|
|
683
|
+
handleCommand(figma.command);
|
|
684
|
+
} else {
|
|
685
|
+
// Default behavior (backward compatibility)
|
|
686
|
+
runGenerate();
|
|
687
|
+
}
|