inkbridge 0.1.0-beta.2 → 0.1.0-beta.21
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 +108 -25
- package/bin/inkbridge.mjs +354 -83
- package/code.js +40 -11802
- package/manifest.json +1 -0
- package/package.json +74 -23
- package/scanner/adapter-utils-regression.ts +159 -0
- package/scanner/aspect-percent-position-regression.ts +237 -0
- package/scanner/aspect-ratio-regression.ts +90 -0
- package/scanner/blob-placement-regression.ts +2 -2
- package/scanner/block-cache-regression.ts +195 -0
- package/scanner/bundle-size-regression.ts +50 -0
- package/scanner/child-sizing-matrix-regression.ts +303 -0
- package/scanner/cli.ts +342 -13
- package/scanner/component-scanner.ts +2108 -174
- package/scanner/component-sections-regression.ts +198 -0
- package/scanner/compound-classes-lookup-regression.ts +163 -0
- package/scanner/css-token-reader-regression.ts +7 -6
- package/scanner/css-token-reader.ts +152 -31
- package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
- package/scanner/cva-master-icon-regression.ts +315 -0
- package/scanner/data-attr-prop-alias-regression.ts +129 -0
- package/scanner/explicit-size-root-regression.ts +102 -0
- package/scanner/font-family-extract-regression.ts +113 -0
- package/scanner/font-style-resolver-regression.ts +1 -1
- package/scanner/framework-adapter-shadcn-regression.ts +480 -0
- package/scanner/full-width-matrix-regression.ts +338 -0
- package/scanner/grid-cols-extraction-regression.ts +110 -0
- package/scanner/image-src-collector-regression.ts +204 -0
- package/scanner/inline-flex-regression.ts +235 -0
- package/scanner/input-range-regression.ts +217 -0
- package/scanner/instance-rendering-regression.ts +224 -0
- package/scanner/jsx-prop-unresolved-regression.ts +178 -0
- package/scanner/jsx-text-regression.ts +178 -0
- package/scanner/layout-alignment-regression.ts +108 -0
- package/scanner/layout-flex-regression.ts +90 -0
- package/scanner/layout-mode-regression.ts +71 -0
- package/scanner/layout-sizing-regression.ts +227 -0
- package/scanner/layout-spacing-regression.ts +135 -0
- package/scanner/local-const-className-regression.ts +331 -0
- package/scanner/percent-position-regression.ts +105 -0
- package/scanner/provider-cascade-regression.ts +224 -0
- package/scanner/provider-flatten-regression.ts +235 -0
- package/scanner/radial-gradient-regression.ts +1 -1
- package/scanner/render-prop-parser-regression.ts +161 -0
- package/scanner/ring-utility-regression.ts +153 -0
- package/scanner/sandbox-spread-regression.ts +125 -0
- package/scanner/selection-pressed-regression.ts +241 -0
- package/scanner/size-full-normalization-regression.ts +127 -0
- package/scanner/state-classification-regression.ts +175 -0
- package/scanner/story-diagnostics-regression.ts +216 -0
- package/scanner/story-dimensioning-regression.ts +298 -0
- package/scanner/story-render-strategy-regression.ts +205 -0
- package/scanner/stretch-to-parent-width-regression.ts +147 -0
- package/scanner/svg-fill-parent-regression.ts +98 -0
- package/scanner/svg-group-inheritance-regression.ts +166 -0
- package/scanner/svg-marker-inline-regression.ts +211 -0
- package/scanner/svg-marker-regression.ts +116 -0
- package/scanner/tailwind-parser.ts +46 -4
- package/scanner/text-resize-matrix-regression.ts +173 -0
- package/scanner/transform-math-regression.ts +1 -1
- package/scanner/types.ts +26 -2
- package/src/cache/frame-cache.ts +150 -0
- package/src/cache/index.ts +2 -0
- package/src/{component-defs.ts → components/component-defs.ts} +25 -10
- package/src/{component-gen.ts → components/component-gen.ts} +43 -116
- package/src/components/component-instance.ts +386 -0
- package/src/components/component-library.ts +44 -0
- package/src/components/component-lookup.ts +161 -0
- package/src/components/index.ts +7 -0
- package/src/components/scanner-types.ts +39 -0
- package/src/components/symbol-instance-policy.ts +312 -0
- package/src/design-system/block-cache.ts +130 -0
- package/src/design-system/component-sections.ts +107 -0
- package/src/design-system/cva-inference.ts +187 -0
- package/src/design-system/cva-master.ts +427 -0
- package/src/design-system/cva-utils.ts +29 -0
- package/src/design-system/design-system.ts +334 -0
- package/src/design-system/frame-stabilizers.ts +191 -0
- package/src/design-system/frame-utils.ts +46 -0
- package/src/design-system/generated-node.ts +84 -0
- package/src/design-system/icon-rendering.ts +229 -0
- package/src/design-system/index.ts +13 -0
- package/src/design-system/instance-rendering.ts +307 -0
- package/src/design-system/master-shared.ts +133 -0
- package/src/design-system/node-helpers.ts +237 -0
- package/src/design-system/node-variants.ts +196 -0
- package/src/design-system/non-cva-master.ts +104 -0
- package/src/design-system/portal-handling.ts +138 -0
- package/src/design-system/preview-builder.ts +738 -0
- package/src/{render-context.ts → design-system/render-context.ts} +32 -6
- package/src/design-system/render-prop-parser.ts +50 -0
- package/src/design-system/responsive-resolver.ts +180 -0
- package/src/design-system/selectable-state.ts +157 -0
- package/src/design-system/state-master.ts +267 -0
- package/src/design-system/state-utils.ts +15 -0
- package/src/design-system/story-builder-context.ts +40 -0
- package/src/design-system/story-builder.ts +1322 -0
- package/src/design-system/story-diagnostics.ts +80 -0
- package/src/design-system/story-dimensioning.ts +272 -0
- package/src/design-system/story-frames.ts +400 -0
- package/src/design-system/story-instance.ts +333 -0
- package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
- package/src/design-system/story-render-strategy.ts +150 -0
- package/src/design-system/story-tree-search.ts +110 -0
- package/src/design-system/symbol-fallback.ts +89 -0
- package/src/design-system/symbol-source.ts +172 -0
- package/src/design-system/table-helpers.ts +56 -0
- package/src/design-system/tag-predicates.ts +99 -0
- package/src/design-system/theme-context.ts +52 -0
- package/src/design-system/typography.ts +100 -0
- package/src/design-system/ui-builder.ts +2676 -0
- package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
- package/src/effects/icon-builder.ts +1074 -0
- package/src/effects/index.ts +5 -0
- package/src/effects/portal-panel.ts +369 -0
- package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
- package/src/framework-adapters/index.ts +47 -0
- package/src/framework-adapters/shadcn.ts +541 -0
- package/src/{github.ts → github/github.ts} +46 -21
- package/src/github/index.ts +1 -0
- package/src/layout/deferred-layout.ts +1556 -0
- package/src/layout/index.ts +24 -0
- package/src/layout/layout-parser.ts +375 -0
- package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
- package/src/layout/parser/alignment.ts +54 -0
- package/src/layout/parser/flex.ts +59 -0
- package/src/layout/parser/index.ts +65 -0
- package/src/layout/parser/ir.ts +80 -0
- package/src/layout/parser/layout-mode.ts +57 -0
- package/src/layout/parser/sizing.ts +241 -0
- package/src/layout/parser/spacing-scale.ts +78 -0
- package/src/layout/parser/spacing.ts +134 -0
- package/src/layout/ring-utils.ts +120 -0
- package/src/layout/size-utils.ts +143 -0
- package/src/layout/text-resize-decision.ts +51 -0
- package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
- package/src/main.ts +444 -162
- package/src/{config.ts → plugin/config.ts} +12 -12
- package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
- package/src/plugin/image-src-collector.ts +52 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin/packs/index.ts +2 -0
- package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
- package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
- package/src/render-engine-version.ts +2 -0
- package/src/tailwind/adapter-utils.ts +137 -0
- package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
- package/src/tailwind/index.ts +8 -0
- package/src/tailwind/jsx-utils.ts +319 -0
- package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
- package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
- package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
- package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
- package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
- package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
- package/src/text/index.ts +4 -0
- package/src/{inline-text.ts → text/inline-text.ts} +13 -13
- package/src/{text-builder.ts → text/text-builder.ts} +24 -7
- package/src/{text-line.ts → text/text-line.ts} +2 -2
- package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
- package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
- package/src/{colors.ts → tokens/colors.ts} +13 -6
- package/src/tokens/index.ts +6 -0
- package/src/{token-source.ts → tokens/token-source.ts} +4 -1
- package/src/{tokens.ts → tokens/tokens.ts} +116 -20
- package/src/{variables.ts → tokens/variables.ts} +447 -102
- package/templates/patch-tokens-route.ts +25 -6
- package/templates/scan-components-route.ts +26 -5
- package/ui.html +485 -37
- package/src/component-lookup.ts +0 -82
- package/src/design-system.ts +0 -59
- package/src/icon-builder.ts +0 -607
- package/src/layout-parser.ts +0 -667
- package/src/story-builder.ts +0 -1706
- package/src/ui-builder.ts +0 -1996
- /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
- /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
- /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
package/src/main.ts
CHANGED
|
@@ -3,24 +3,147 @@
|
|
|
3
3
|
// Includes "Push to Code" feature for GitHub PR creation.
|
|
4
4
|
// Universal plugin - configure for any project via Settings.
|
|
5
5
|
|
|
6
|
-
import { loadConfig, saveConfig, GITHUB_CONFIG } from './
|
|
6
|
+
import { loadConfig, saveConfig, GITHUB_CONFIG } from './plugin';
|
|
7
7
|
import { TOKENS, COMPONENT_DEFS, applyScannedTokens, getCoreFontFamily, getThemeFontFamily, getThemeNames } from './tokens';
|
|
8
|
-
import { rebuildColorIndex } from './
|
|
9
|
-
import { handleUIReady, handleImageResult, prefetchImages } from './
|
|
10
|
-
import { setImageMap, setSvgMap } from './
|
|
11
|
-
import { applyPack, type Pack } from './
|
|
12
|
-
import {
|
|
13
|
-
import { createOrUpdateStyles } from './variables';
|
|
8
|
+
import { rebuildColorIndex } from './tokens';
|
|
9
|
+
import { handleUIReady, handleImageResult, prefetchImages } from './plugin';
|
|
10
|
+
import { setImageMap, setSvgMap } from './cache';
|
|
11
|
+
import { applyPack, type Pack, handlePackResult, refreshPack } from './plugin';
|
|
12
|
+
import { createOrUpdateStyles } from './tokens';
|
|
14
13
|
import { tailwindForNode } from './tailwind';
|
|
15
|
-
import { detectComponentChanges } from './
|
|
14
|
+
import { detectComponentChanges } from './tokens';
|
|
16
15
|
import { pushToGitHub, pushToGitHubWithComponents, handleGitHubFetchResult, handlePatchCssResult, previewTokenChanges } from './github';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
import {
|
|
17
|
+
buildDesignSystemSinglePage,
|
|
18
|
+
cleanGeneratedDesignSystemArtifacts,
|
|
19
|
+
computePreflightData,
|
|
20
|
+
isGeneratedDesignSystemNode,
|
|
21
|
+
} from './design-system';
|
|
22
|
+
import { hashString, stableStringify, getFrameHash, findChildByName } from './cache';
|
|
23
|
+
import { waitForAllFonts } from './text';
|
|
24
|
+
import { getKnownStylesForFamily, initializeFontStyleIndex } from './text';
|
|
25
|
+
import type { ResolvedTokenSourceMode, ScannedTokenMap, TokenSourceMode } from './tokens';
|
|
26
|
+
import { collectImageSrcs } from './plugin/image-src-collector';
|
|
21
27
|
|
|
22
28
|
const PACK_LOAD_ERROR = 'Dev server not reachable / pack not found.';
|
|
29
|
+
const PREFLIGHT_EXCLUDED_KEY = 'preflight_excluded_components';
|
|
30
|
+
// Synthetic preflight identifier for the Design Tokens row. Kept as a constant
|
|
31
|
+
// so design-system.ts and main.ts agree on the magic name. Double-underscore
|
|
32
|
+
// prefix avoids collision with real component names.
|
|
33
|
+
const DESIGN_TOKENS_TOGGLE = '__design_tokens__';
|
|
34
|
+
|
|
35
|
+
// Loading-phase status messages render inside the plugin panel (above the
|
|
36
|
+
// drop animation) instead of as Figma toasts. The panel must be visible
|
|
37
|
+
// for these to be seen, so the caller is responsible for showing the
|
|
38
|
+
// loading view before invoking setStatus. Errors and final success still
|
|
39
|
+
// use figma.notify so they're visible even if the panel is closed.
|
|
40
|
+
/**
|
|
41
|
+
* Loading-panel status. Accepts either a flat string (legacy single-line
|
|
42
|
+
* status) or `{ phase, detail }` where the phase is sticky context
|
|
43
|
+
* ("Building components") and the detail is the per-section sub-line
|
|
44
|
+
* ("Primary theme · Organisms (26)"). Both forms post a `status`
|
|
45
|
+
* message; the UI iframe renders them as one or two lines.
|
|
46
|
+
*
|
|
47
|
+
* Two yield modes:
|
|
48
|
+
*
|
|
49
|
+
* - `noYield: true` (fine-grained per-section emits) — does a
|
|
50
|
+
* `setTimeout(0)` yield, just long enough for the iframe to drain its
|
|
51
|
+
* postMessage queue and update the panel. Without any yield the
|
|
52
|
+
* iframe never gets a turn while code.js holds the thread, so the
|
|
53
|
+
* user only ever sees the parent phase ("Building components") even
|
|
54
|
+
* though 20+ detail emits fire. With a 0ms yield the panel updates
|
|
55
|
+
* but Figma's canvas typically can't complete a full paint pass in
|
|
56
|
+
* that window — combined with the column visibility-toggle in
|
|
57
|
+
* addColumn, no overlap is observable.
|
|
58
|
+
* - default (coarse phase boundaries in main.ts) — 50ms yield. Gives
|
|
59
|
+
* the canvas a paint frame so users see progress between top-level
|
|
60
|
+
* phases (Reading components → Building tokens → Building components).
|
|
61
|
+
*/
|
|
62
|
+
type StatusInput =
|
|
63
|
+
| string
|
|
64
|
+
| { phase?: string; detail?: string; noYield?: boolean };
|
|
65
|
+
|
|
66
|
+
async function setStatus(input: StatusInput): Promise<void> {
|
|
67
|
+
const noYield = typeof input === 'object' && input !== null && input.noYield === true;
|
|
68
|
+
// Only include `phase` / `detail` in the message when the caller
|
|
69
|
+
// actually provided them. Sending `phase: ''` on a detail-only emit
|
|
70
|
+
// would clobber the sticky phase header in the UI — the panel relies
|
|
71
|
+
// on the absence of a `phase` key to keep the previous phase visible
|
|
72
|
+
// while detail messages cycle below.
|
|
73
|
+
//
|
|
74
|
+
// A bare string is treated as a NEW phase (sticky header) with the
|
|
75
|
+
// detail cleared. CSS animates trailing dots on the phase element, so
|
|
76
|
+
// we strip any baked-in `…` / `...` ellipsis from both phase AND
|
|
77
|
+
// detail — otherwise the visible text would carry static dots
|
|
78
|
+
// alongside the animated dots from CSS, masking the animation.
|
|
79
|
+
const stripTrailingDots = (text: string): string => text.replace(/(\u2026|\.\.\.)\s*$/, '').trim();
|
|
80
|
+
let message: {
|
|
81
|
+
type: 'status';
|
|
82
|
+
phase?: string;
|
|
83
|
+
detail?: string;
|
|
84
|
+
};
|
|
85
|
+
if (typeof input === 'string') {
|
|
86
|
+
message = { type: 'status', phase: stripTrailingDots(input), detail: '' };
|
|
87
|
+
} else {
|
|
88
|
+
message = { type: 'status' };
|
|
89
|
+
if (typeof input.phase === 'string') message.phase = stripTrailingDots(input.phase);
|
|
90
|
+
if (typeof input.detail === 'string') message.detail = stripTrailingDots(input.detail);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
figma.ui.postMessage(message);
|
|
94
|
+
} catch (_err) {
|
|
95
|
+
// ui not yet shown — caller should show the loading view first
|
|
96
|
+
}
|
|
97
|
+
const delay = noYield ? 0 : 50;
|
|
98
|
+
await new Promise(function (resolve) { setTimeout(resolve, delay); });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function clearStatus(): void {
|
|
102
|
+
try {
|
|
103
|
+
figma.ui.postMessage({ type: 'status', phase: '', detail: '' });
|
|
104
|
+
} catch (_err) {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
function errorMessage(e: unknown): string {
|
|
109
|
+
if (e instanceof Error) return errorMessage(e);
|
|
110
|
+
if (e && typeof e === 'object' && 'message' in e) return String((e as { message: unknown }).message);
|
|
111
|
+
return String(e);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function loadExcludedComponents(): Promise<string[]> {
|
|
115
|
+
const docId = (figma.root && figma.root.id) ? String(figma.root.id) : '';
|
|
116
|
+
const key = docId ? PREFLIGHT_EXCLUDED_KEY + ':' + docId : PREFLIGHT_EXCLUDED_KEY;
|
|
117
|
+
try {
|
|
118
|
+
const stored = await figma.clientStorage.getAsync(key);
|
|
119
|
+
if (Array.isArray(stored)) return stored as string[];
|
|
120
|
+
} catch (_e) {}
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function saveExcludedComponents(excluded: string[]): Promise<void> {
|
|
125
|
+
const docId = (figma.root && figma.root.id) ? String(figma.root.id) : '';
|
|
126
|
+
const key = docId ? PREFLIGHT_EXCLUDED_KEY + ':' + docId : PREFLIGHT_EXCLUDED_KEY;
|
|
127
|
+
try {
|
|
128
|
+
await figma.clientStorage.setAsync(key, excluded);
|
|
129
|
+
} catch (_e) {}
|
|
130
|
+
}
|
|
23
131
|
const TOKEN_SOURCE_INFO_KEY = 'token_source_info';
|
|
132
|
+
const DEBUG_PLUGIN_DATA_KEYS = [
|
|
133
|
+
'inkbridge:generated',
|
|
134
|
+
'inkbridge:scope',
|
|
135
|
+
'inkbridge:role',
|
|
136
|
+
'inkbridge:fallback-reason',
|
|
137
|
+
'inkbridge:hash',
|
|
138
|
+
'inkbridge:symbol-decision',
|
|
139
|
+
'inkbridge:symbol-ignored-props',
|
|
140
|
+
'inkbridge:symbol-text-overrides',
|
|
141
|
+
'inkbridge:symbol-slot-prop-mappings',
|
|
142
|
+
'inkbridge:story-name',
|
|
143
|
+
'inkbridge:story-def',
|
|
144
|
+
'inkbridge:story-scan',
|
|
145
|
+
'inkbridge:story-render',
|
|
146
|
+
];
|
|
24
147
|
|
|
25
148
|
type TokenSourceInfo = {
|
|
26
149
|
source: string;
|
|
@@ -30,6 +153,106 @@ type TokenSourceInfo = {
|
|
|
30
153
|
|
|
31
154
|
let LAST_TOKEN_SOURCE_INFO: TokenSourceInfo | null = null;
|
|
32
155
|
|
|
156
|
+
function readNodePluginData(node: BaseNode): Record<string, string> {
|
|
157
|
+
const out: Record<string, string> = {};
|
|
158
|
+
if (!node || typeof node.getPluginData !== 'function') return out;
|
|
159
|
+
for (let i = 0; i < DEBUG_PLUGIN_DATA_KEYS.length; i++) {
|
|
160
|
+
const key = DEBUG_PLUGIN_DATA_KEYS[i];
|
|
161
|
+
try {
|
|
162
|
+
const value = node.getPluginData(key);
|
|
163
|
+
if (value) out[key] = value;
|
|
164
|
+
} catch (_err) {
|
|
165
|
+
// ignore plugin data read failures
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
type NodeDescription = {
|
|
172
|
+
id: string;
|
|
173
|
+
name: string;
|
|
174
|
+
type: string;
|
|
175
|
+
width?: number;
|
|
176
|
+
height?: number;
|
|
177
|
+
layoutMode?: string;
|
|
178
|
+
childCount?: number;
|
|
179
|
+
pluginData?: Record<string, string>;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
function describeNode(node: BaseNode): NodeDescription {
|
|
183
|
+
const info: NodeDescription = {
|
|
184
|
+
id: node && node.id ? String(node.id) : '',
|
|
185
|
+
name: node && node.name ? String(node.name) : '',
|
|
186
|
+
type: node && node.type ? String(node.type) : '',
|
|
187
|
+
};
|
|
188
|
+
if (node && 'width' in node && typeof node.width === 'number') info.width = Math.round(node.width);
|
|
189
|
+
if (node && 'height' in node && typeof node.height === 'number') info.height = Math.round(node.height);
|
|
190
|
+
if (node && 'layoutMode' in node && node.layoutMode) info.layoutMode = String(node.layoutMode);
|
|
191
|
+
if (node && 'children' in node && Array.isArray(node.children)) {
|
|
192
|
+
info.childCount = node.children.length;
|
|
193
|
+
}
|
|
194
|
+
const pluginData = readNodePluginData(node);
|
|
195
|
+
if (Object.keys(pluginData).length > 0) {
|
|
196
|
+
info.pluginData = pluginData;
|
|
197
|
+
}
|
|
198
|
+
return info;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function collectChildrenSummary(node: BaseNode): NodeDescription[] {
|
|
202
|
+
if (!node || !('children' in node) || !Array.isArray(node.children)) return [];
|
|
203
|
+
const out: NodeDescription[] = [];
|
|
204
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
205
|
+
out.push(describeNode(node.children[i]));
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function findGeneratedDebugFallbackNode(): BaseNode | null {
|
|
211
|
+
const currentPage = figma.currentPage;
|
|
212
|
+
if (currentPage && isGeneratedDesignSystemNode(currentPage)) {
|
|
213
|
+
return currentPage;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (figma.root && Array.isArray(figma.root.children)) {
|
|
217
|
+
for (let i = 0; i < figma.root.children.length; i++) {
|
|
218
|
+
const page = figma.root.children[i];
|
|
219
|
+
if (!page || page.type !== 'PAGE') continue;
|
|
220
|
+
if (isGeneratedDesignSystemNode(page)) return page;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function runDebugSelectionCommand(): void {
|
|
228
|
+
const selection = figma.currentPage && Array.isArray(figma.currentPage.selection)
|
|
229
|
+
? figma.currentPage.selection
|
|
230
|
+
: [];
|
|
231
|
+
const fallbackNode = selection.length === 0 ? findGeneratedDebugFallbackNode() : null;
|
|
232
|
+
const targetNodes = selection.length > 0
|
|
233
|
+
? selection
|
|
234
|
+
: (fallbackNode ? [fallbackNode] : []);
|
|
235
|
+
if (targetNodes.length === 0) {
|
|
236
|
+
figma.notify('No selected node and no generated Design System page found.');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const report = targetNodes.map(function(node: BaseNode) {
|
|
241
|
+
return {
|
|
242
|
+
selected: describeNode(node),
|
|
243
|
+
children: collectChildrenSummary(node),
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
console.error('[Inkbridge][DebugSelection]', JSON.stringify(report, null, 2));
|
|
248
|
+
if (selection.length > 0) {
|
|
249
|
+
figma.notify('Debug report written to plugin console.');
|
|
250
|
+
} else {
|
|
251
|
+
figma.notify('Debug report written for generated Design System page.');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
33
256
|
function getPackLoadErrorMessage(error?: string): string {
|
|
34
257
|
if (error === 'incompatible-schema-version' || error === 'incompatible-pack-version') {
|
|
35
258
|
return 'Incompatible scanner contract. Update plugin and scanner to matching versions.';
|
|
@@ -44,18 +267,16 @@ function isOfflineError(error?: string): boolean {
|
|
|
44
267
|
return !error || error === 'timeout' || error === 'no-result';
|
|
45
268
|
}
|
|
46
269
|
|
|
47
|
-
function coerceTokenSourceInfo(raw:
|
|
270
|
+
function coerceTokenSourceInfo(raw: unknown): TokenSourceInfo {
|
|
271
|
+
const obj = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : null;
|
|
272
|
+
const modeRaw = obj ? obj.mode : null;
|
|
48
273
|
const mode: ResolvedTokenSourceMode =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
: 'embedded';
|
|
274
|
+
modeRaw === 'css' || modeRaw === 'dtcg' || modeRaw === 'embedded' ? modeRaw : 'embedded';
|
|
275
|
+
const requestedRaw = obj ? obj.requestedMode : null;
|
|
52
276
|
const requestedMode: TokenSourceMode | undefined =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const source = raw && typeof raw.source === 'string' && raw.source.trim()
|
|
57
|
-
? raw.source.trim()
|
|
58
|
-
: 'embedded:tokens.ts';
|
|
277
|
+
requestedRaw === 'css' || requestedRaw === 'dtcg' ? requestedRaw : undefined;
|
|
278
|
+
const sourceRaw = obj && typeof obj.source === 'string' ? obj.source.trim() : '';
|
|
279
|
+
const source = sourceRaw || 'embedded:tokens.ts';
|
|
59
280
|
return { source, mode, requestedMode };
|
|
60
281
|
}
|
|
61
282
|
|
|
@@ -83,41 +304,11 @@ async function postTokenSourceInfoToUI(): Promise<void> {
|
|
|
83
304
|
figma.ui.postMessage({ type: 'token-source-info', info: info });
|
|
84
305
|
}
|
|
85
306
|
|
|
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
307
|
// ----------------------- License Checking -----------------------
|
|
118
308
|
|
|
119
309
|
let pendingLicenseResolve: ((tier: 'free' | 'pro') => void) | null = null;
|
|
120
310
|
let licenseValidatingFromSettings = false;
|
|
311
|
+
let sessionTier: 'free' | 'pro' | null = null;
|
|
121
312
|
|
|
122
313
|
async function checkLicenseOnline(key: string): Promise<'free' | 'pro'> {
|
|
123
314
|
return new Promise<'free' | 'pro'>(function(resolve) {
|
|
@@ -133,8 +324,11 @@ async function checkLicenseOnline(key: string): Promise<'free' | 'pro'> {
|
|
|
133
324
|
}
|
|
134
325
|
|
|
135
326
|
async function checkLicense(): Promise<'free' | 'pro'> {
|
|
327
|
+
// Memory cache — valid for the lifetime of this plugin session
|
|
328
|
+
if (sessionTier !== null) return sessionTier;
|
|
329
|
+
|
|
136
330
|
const key = await figma.clientStorage.getAsync('license_key');
|
|
137
|
-
if (!key) return 'free';
|
|
331
|
+
if (!key) { sessionTier = 'free'; return 'free'; }
|
|
138
332
|
|
|
139
333
|
const cachedTier = await figma.clientStorage.getAsync('license_tier') as string | null;
|
|
140
334
|
const cachedAt = await figma.clientStorage.getAsync('license_cache_at') as string | null;
|
|
@@ -142,10 +336,13 @@ async function checkLicense(): Promise<'free' | 'pro'> {
|
|
|
142
336
|
|
|
143
337
|
// Only trust cached 'pro' — never trust cached 'free' (avoids stale negatives)
|
|
144
338
|
if (cachedTier === 'pro' && cachedAt && (Date.now() - Number(cachedAt)) < SEVEN_DAYS) {
|
|
339
|
+
sessionTier = 'pro';
|
|
145
340
|
return 'pro';
|
|
146
341
|
}
|
|
147
342
|
|
|
148
|
-
|
|
343
|
+
const tier = await checkLicenseOnline(key as string);
|
|
344
|
+
sessionTier = tier;
|
|
345
|
+
return tier;
|
|
149
346
|
}
|
|
150
347
|
|
|
151
348
|
// ----------------------- Generate Command -----------------------
|
|
@@ -184,18 +381,46 @@ async function warmThemeFonts(): Promise<string[]> {
|
|
|
184
381
|
return failedFonts;
|
|
185
382
|
}
|
|
186
383
|
|
|
384
|
+
async function waitForFontsBeforeClose(timeoutMs = 4000): Promise<void> {
|
|
385
|
+
let timedOut = false;
|
|
386
|
+
await Promise.race([
|
|
387
|
+
waitForAllFonts(),
|
|
388
|
+
new Promise<void>((resolve) => {
|
|
389
|
+
setTimeout(() => {
|
|
390
|
+
timedOut = true;
|
|
391
|
+
resolve();
|
|
392
|
+
}, timeoutMs);
|
|
393
|
+
}),
|
|
394
|
+
]);
|
|
395
|
+
if (timedOut) {
|
|
396
|
+
figma.notify('⚠️ Font load timeout; continuing.', { timeout: 2000 });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
187
400
|
async function runGenerate(): Promise<void> {
|
|
188
401
|
let shouldClose = true;
|
|
189
402
|
try {
|
|
190
403
|
await loadConfig();
|
|
191
|
-
// Show UI
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
404
|
+
// Show the UI panel visibly with the loading view from the very start
|
|
405
|
+
// so progress messages render in-panel above the drop animation
|
|
406
|
+
// (handled by setStatus → postMessage). The UI also relays fetch
|
|
407
|
+
// requests since the code.js sandbox has no network access. Match the
|
|
408
|
+
// preflight panel size (520) so the panel doesn't resize-jump when
|
|
409
|
+
// the preflight view replaces the loading view, and so the drop's
|
|
410
|
+
// fall (translateY -300 from `bottom:58`) starts BELOW the status
|
|
411
|
+
// text instead of above it (which happened at height 400, where the
|
|
412
|
+
// drop's start anchored at the top of the panel).
|
|
413
|
+
figma.showUI(__html__, { width: 360, height: 520 });
|
|
414
|
+
figma.ui.postMessage({ type: 'show-view', view: 'loading' });
|
|
415
|
+
// Yield so the UI mounts and applies the loading view before the
|
|
416
|
+
// first setStatus message lands.
|
|
417
|
+
await new Promise(function (resolve) { setTimeout(resolve, 0); });
|
|
418
|
+
|
|
419
|
+
await setStatus('Reading your components…');
|
|
196
420
|
const result = await refreshPack(GITHUB_CONFIG || undefined);
|
|
197
421
|
|
|
198
422
|
if (!result.success || !result.pack) {
|
|
423
|
+
clearStatus();
|
|
199
424
|
const message = getPackLoadErrorMessage(result.error);
|
|
200
425
|
figma.notify('❌ ' + message);
|
|
201
426
|
figma.showUI(__html__, { width: 360, height: 400 });
|
|
@@ -217,15 +442,36 @@ async function runGenerate(): Promise<void> {
|
|
|
217
442
|
applyScannedTokens(result.pack.tokens);
|
|
218
443
|
await saveAndBroadcastTokenSourceInfo(getTokenSourceInfoFromPack(result.pack));
|
|
219
444
|
|
|
445
|
+
// Empty-state guard: the dev server is reachable but the scan
|
|
446
|
+
// returned no usable components (no `.stories.tsx` / `.stories.ts`
|
|
447
|
+
// found under the configured paths). Stop here and show an
|
|
448
|
+
// actionable message instead of generating an empty page.
|
|
449
|
+
if (!COMPONENT_DEFS.components || COMPONENT_DEFS.components.length === 0) {
|
|
450
|
+
clearStatus();
|
|
451
|
+
figma.showUI(__html__, { width: 360, height: 400 });
|
|
452
|
+
figma.ui.postMessage({ type: 'show-view', view: 'offline' });
|
|
453
|
+
figma.ui.postMessage({
|
|
454
|
+
type: 'pack-load-error',
|
|
455
|
+
message:
|
|
456
|
+
'Connected to the dev server, but no Storybook stories were found. ' +
|
|
457
|
+
'Check `inkbridge.config.json:componentPaths`, or add `.stories.tsx` / ' +
|
|
458
|
+
'`.stories.ts` files next to your components. The `pnpm inkbridge:scan` ' +
|
|
459
|
+
'output names every file it inspected if you need details.',
|
|
460
|
+
});
|
|
461
|
+
shouldClose = false;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
await setStatus('Preparing fonts…');
|
|
220
466
|
const failedFonts = await warmThemeFonts();
|
|
221
467
|
if (failedFonts.length) {
|
|
222
468
|
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
469
|
}
|
|
224
|
-
figma.notify('✅ Loaded ' + COMPONENT_DEFS.components.length + ' components');
|
|
225
470
|
|
|
226
471
|
// Pre-fetch all image srcs found in component definitions
|
|
227
|
-
const imageSrcs = collectImageSrcs(COMPONENT_DEFS.components
|
|
472
|
+
const imageSrcs = collectImageSrcs(COMPONENT_DEFS.components);
|
|
228
473
|
if (imageSrcs.length > 0) {
|
|
474
|
+
await setStatus('Preloading images…');
|
|
229
475
|
// Extract port from the source URL (e.g. 'http://localhost:3000/api/...')
|
|
230
476
|
const portMatch = result.source?.match(/localhost:(\d+)/);
|
|
231
477
|
const devPort = portMatch ? parseInt(portMatch[1], 10) : 3000;
|
|
@@ -237,14 +483,45 @@ async function runGenerate(): Promise<void> {
|
|
|
237
483
|
rebuildColorIndex(TOKENS);
|
|
238
484
|
createOrUpdateStyles();
|
|
239
485
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
486
|
+
// Show preflight selection UI before generation
|
|
487
|
+
await setStatus('Detecting changes…');
|
|
488
|
+
const tokenHash = hashString(stableStringify(TOKENS));
|
|
489
|
+
const preflightItems = computePreflightData(tokenHash);
|
|
490
|
+
const excluded = await loadExcludedComponents();
|
|
491
|
+
// Prepend the design-tokens toggle. Compute its status against the
|
|
492
|
+
// existing tokens row's frame-hash so the preflight UI reflects
|
|
493
|
+
// reality: 'new' when the row doesn't exist (e.g. after Clean
|
|
494
|
+
// Generated Artifacts), 'changed' when token values shifted since
|
|
495
|
+
// the last build, 'unchanged' when nothing changed. Previously this
|
|
496
|
+
// was hardcoded to 'new' which made every fresh run show "design
|
|
497
|
+
// tokens changed" even when token values were identical.
|
|
498
|
+
let tokensStatus: 'new' | 'changed' | 'unchanged' = 'new';
|
|
499
|
+
const dsPage = figma.root && Array.isArray(figma.root.children)
|
|
500
|
+
? figma.root.children.find(function (p) { return p.name === 'Design System' && p.type === 'PAGE'; })
|
|
501
|
+
: null;
|
|
502
|
+
if (dsPage) {
|
|
503
|
+
const existingTokensRow = findChildByName(dsPage, 'Design Tokens');
|
|
504
|
+
if (existingTokensRow) {
|
|
505
|
+
tokensStatus = getFrameHash(existingTokensRow) === tokenHash ? 'unchanged' : 'changed';
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
preflightItems.unshift({
|
|
509
|
+
name: DESIGN_TOKENS_TOGGLE,
|
|
510
|
+
displayName: 'Design tokens',
|
|
511
|
+
status: tokensStatus,
|
|
512
|
+
section: '__system__',
|
|
513
|
+
sectionTitle: 'Design system',
|
|
514
|
+
});
|
|
515
|
+
clearStatus();
|
|
516
|
+
figma.showUI(__html__, { visible: true, width: 360, height: 520 });
|
|
517
|
+
figma.ui.postMessage({ type: 'show-preflight', items: preflightItems, excluded: excluded });
|
|
518
|
+
shouldClose = false;
|
|
519
|
+
} catch (e) {
|
|
520
|
+
clearStatus();
|
|
521
|
+
figma.notify('❌ Failed: ' + errorMessage(e));
|
|
245
522
|
} finally {
|
|
246
523
|
if (shouldClose) {
|
|
247
|
-
await
|
|
524
|
+
await waitForFontsBeforeClose();
|
|
248
525
|
figma.closePlugin();
|
|
249
526
|
}
|
|
250
527
|
}
|
|
@@ -252,77 +529,6 @@ async function runGenerate(): Promise<void> {
|
|
|
252
529
|
|
|
253
530
|
// ----------------------- Menu Command Handling -----------------------
|
|
254
531
|
|
|
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
532
|
async function handleCommand(command: string): Promise<void> {
|
|
327
533
|
switch (command) {
|
|
328
534
|
case 'push': {
|
|
@@ -367,8 +573,19 @@ async function handleCommand(command: string): Promise<void> {
|
|
|
367
573
|
break;
|
|
368
574
|
}
|
|
369
575
|
|
|
576
|
+
case 'clean-generated': {
|
|
577
|
+
const result = cleanGeneratedDesignSystemArtifacts();
|
|
578
|
+
if (result.removedNodes > 0) {
|
|
579
|
+
figma.notify('✅ Removed ' + result.removedNodes + ' generated node(s) across ' + result.touchedPages + ' page(s).');
|
|
580
|
+
} else {
|
|
581
|
+
figma.notify('No generated artifacts found.');
|
|
582
|
+
}
|
|
583
|
+
figma.closePlugin();
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
|
|
370
587
|
case 'debug-selection': {
|
|
371
|
-
|
|
588
|
+
runDebugSelectionCommand();
|
|
372
589
|
figma.closePlugin();
|
|
373
590
|
break;
|
|
374
591
|
}
|
|
@@ -382,7 +599,46 @@ async function handleCommand(command: string): Promise<void> {
|
|
|
382
599
|
|
|
383
600
|
// ----------------------- UI Message Handler -----------------------
|
|
384
601
|
|
|
602
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- UI postMessage payloads are untyped JSON
|
|
385
603
|
figma.ui.onmessage = async (msg: any) => {
|
|
604
|
+
if (msg.type === 'confirm-preflight') {
|
|
605
|
+
const excluded: string[] = Array.isArray(msg.excluded) ? msg.excluded as string[] : [];
|
|
606
|
+
await saveExcludedComponents(excluded);
|
|
607
|
+
// The UI is already visible (the preflight view sent this message), so
|
|
608
|
+
// just switch to the empty loading view rather than re-invoking
|
|
609
|
+
// `showUI` — reinvocation triggers an iframe rerender that leaves the
|
|
610
|
+
// window blank until the UI finishes reloading. The loading view is
|
|
611
|
+
// intentionally empty; only the bottom bridge watermark stays visible
|
|
612
|
+
// while the toast handles phase feedback.
|
|
613
|
+
figma.ui.postMessage({ type: 'show-view', view: 'loading' });
|
|
614
|
+
// Let the UI process the view-switch before we block the main thread
|
|
615
|
+
// with the synchronous build work.
|
|
616
|
+
await new Promise(function (resolve) { setTimeout(resolve, 0); });
|
|
617
|
+
try {
|
|
618
|
+
await buildDesignSystemSinglePage(excluded, { onStatus: setStatus });
|
|
619
|
+
clearStatus();
|
|
620
|
+
figma.notify('✅ Design system created');
|
|
621
|
+
} catch (e) {
|
|
622
|
+
clearStatus();
|
|
623
|
+
const message = errorMessage(e);
|
|
624
|
+
const stack = e instanceof Error && e.stack ? e.stack : '';
|
|
625
|
+
if (stack) {
|
|
626
|
+
console.error('[Inkbridge][BuildFailed]', stack);
|
|
627
|
+
} else {
|
|
628
|
+
console.error('[Inkbridge][BuildFailed]', message);
|
|
629
|
+
}
|
|
630
|
+
figma.notify('❌ Build failed: ' + message, { timeout: 10000 });
|
|
631
|
+
}
|
|
632
|
+
await waitForFontsBeforeClose();
|
|
633
|
+
figma.closePlugin();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (msg.type === 'cancel-preflight') {
|
|
638
|
+
figma.closePlugin();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
386
642
|
// Handle UI iframe ready signal
|
|
387
643
|
if (msg.type === 'ui-ready') {
|
|
388
644
|
handleUIReady();
|
|
@@ -392,6 +648,7 @@ figma.ui.onmessage = async (msg: any) => {
|
|
|
392
648
|
// Handle license validation result from UI iframe
|
|
393
649
|
if (msg.type === 'license-result') {
|
|
394
650
|
const tier: 'free' | 'pro' = msg.tier === 'pro' ? 'pro' : 'free';
|
|
651
|
+
sessionTier = tier;
|
|
395
652
|
await figma.clientStorage.setAsync('license_tier', tier);
|
|
396
653
|
await figma.clientStorage.setAsync('license_cache_at', String(Date.now()));
|
|
397
654
|
if (pendingLicenseResolve) {
|
|
@@ -444,8 +701,8 @@ figma.ui.onmessage = async (msg: any) => {
|
|
|
444
701
|
setTimeout(() => {
|
|
445
702
|
figma.ui.postMessage({ type: 'show-view', view: 'overview' });
|
|
446
703
|
}, 1000);
|
|
447
|
-
} catch (e
|
|
448
|
-
figma.ui.postMessage({ type: 'config-status', message: 'Failed to save token: ' + e
|
|
704
|
+
} catch (e) {
|
|
705
|
+
figma.ui.postMessage({ type: 'config-status', message: 'Failed to save token: ' + errorMessage(e), success: false });
|
|
449
706
|
}
|
|
450
707
|
}
|
|
451
708
|
|
|
@@ -456,7 +713,7 @@ figma.ui.onmessage = async (msg: any) => {
|
|
|
456
713
|
repo: msg.repo || '',
|
|
457
714
|
baseBranch: msg.baseBranch || 'main',
|
|
458
715
|
tokenPath: msg.tokenPath || 'design-tokens/tokens.dtcg.json',
|
|
459
|
-
tokenSourceMode: msg.tokenSourceMode || '
|
|
716
|
+
tokenSourceMode: msg.tokenSourceMode || 'css',
|
|
460
717
|
cssTokenPath: msg.cssTokenPath || '',
|
|
461
718
|
syncDtcgOnPush: msg.syncDtcgOnPush === true,
|
|
462
719
|
allowNewTokensFromFigma: msg.allowNewTokensFromFigma === true,
|
|
@@ -486,22 +743,32 @@ figma.ui.onmessage = async (msg: any) => {
|
|
|
486
743
|
figma.ui.postMessage({ type: 'show-view', view: 'overview' });
|
|
487
744
|
}, 800);
|
|
488
745
|
}
|
|
489
|
-
} catch (e
|
|
490
|
-
figma.ui.postMessage({ type: 'settings-status', message: 'Failed: ' + e
|
|
746
|
+
} catch (e) {
|
|
747
|
+
figma.ui.postMessage({ type: 'settings-status', message: 'Failed: ' + errorMessage(e), success: false });
|
|
491
748
|
}
|
|
492
749
|
}
|
|
493
750
|
|
|
494
751
|
if (msg.type === 'push-to-github') {
|
|
752
|
+
const pushTier = await checkLicense();
|
|
753
|
+
if (pushTier !== 'pro') {
|
|
754
|
+
figma.ui.postMessage({ type: 'show-view', view: 'upgrade' });
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
495
757
|
try {
|
|
496
758
|
figma.ui.postMessage({ type: 'push-status', message: 'Creating branch...', success: null });
|
|
497
759
|
const prUrl = await pushToGitHub(msg.commitMessage, msg.prDescription);
|
|
498
760
|
figma.ui.postMessage({ type: 'push-status', message: 'PR created!', success: true, url: prUrl });
|
|
499
|
-
} catch (e
|
|
500
|
-
figma.ui.postMessage({ type: 'push-status', message: 'Failed: ' + e
|
|
761
|
+
} catch (e) {
|
|
762
|
+
figma.ui.postMessage({ type: 'push-status', message: 'Failed: ' + errorMessage(e), success: false });
|
|
501
763
|
}
|
|
502
764
|
}
|
|
503
765
|
|
|
504
766
|
if (msg.type === 'detect-changes') {
|
|
767
|
+
const detectTier = await checkLicense();
|
|
768
|
+
if (detectTier !== 'pro') {
|
|
769
|
+
figma.ui.postMessage({ type: 'show-view', view: 'upgrade' });
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
505
772
|
try {
|
|
506
773
|
await loadConfig();
|
|
507
774
|
const pushToken = await figma.clientStorage.getAsync('github_token') as string | null;
|
|
@@ -532,16 +799,16 @@ figma.ui.onmessage = async (msg: any) => {
|
|
|
532
799
|
|
|
533
800
|
// Detect changes between Figma and code
|
|
534
801
|
const changes = detectComponentChanges();
|
|
535
|
-
if (
|
|
802
|
+
if (changes.error) {
|
|
536
803
|
figma.showUI(__html__, { width: 360, height: 520 });
|
|
537
804
|
figma.ui.postMessage({ type: 'show-view', view: 'sync' });
|
|
538
805
|
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!pushToken });
|
|
539
806
|
await postTokenSourceInfoToUI();
|
|
540
807
|
figma.ui.postMessage({ type: 'changes-detected', changes: { tokens: false, components: [] } });
|
|
541
|
-
figma.ui.postMessage({ type: 'sync-status', message: String(
|
|
808
|
+
figma.ui.postMessage({ type: 'sync-status', message: String(changes.error), success: false });
|
|
542
809
|
return;
|
|
543
810
|
}
|
|
544
|
-
const componentChanges = Array.isArray(
|
|
811
|
+
const componentChanges = Array.isArray(changes.changes) ? changes.changes : [];
|
|
545
812
|
const tokenPreview = await previewTokenChanges(pushToken);
|
|
546
813
|
const hasTokenChanges = tokenPreview.hasChanges;
|
|
547
814
|
|
|
@@ -565,14 +832,19 @@ figma.ui.onmessage = async (msg: any) => {
|
|
|
565
832
|
figma.ui.postMessage({ type: 'config-loaded', config: GITHUB_CONFIG, hasToken: !!pushToken });
|
|
566
833
|
await postTokenSourceInfoToUI();
|
|
567
834
|
figma.ui.postMessage({ type: 'changes-detected', changes: { tokens: hasTokenChanges, components: componentChanges } });
|
|
568
|
-
} catch (e
|
|
835
|
+
} catch (e) {
|
|
569
836
|
figma.showUI(__html__, { width: 360, height: 520 });
|
|
570
837
|
figma.ui.postMessage({ type: 'show-view', view: 'sync' });
|
|
571
|
-
figma.ui.postMessage({ type: 'sync-status', message: 'Failed: ' + e
|
|
838
|
+
figma.ui.postMessage({ type: 'sync-status', message: 'Failed: ' + errorMessage(e), success: false });
|
|
572
839
|
}
|
|
573
840
|
}
|
|
574
841
|
|
|
575
842
|
if (msg.type === 'sync-to-github') {
|
|
843
|
+
const syncTier = await checkLicense();
|
|
844
|
+
if (syncTier !== 'pro') {
|
|
845
|
+
figma.ui.postMessage({ type: 'show-view', view: 'upgrade' });
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
576
848
|
try {
|
|
577
849
|
figma.notify('Sync request received', { timeout: 1200 });
|
|
578
850
|
figma.ui.postMessage({ type: 'sync-status', message: 'Starting sync...', success: null });
|
|
@@ -586,13 +858,18 @@ figma.ui.onmessage = async (msg: any) => {
|
|
|
586
858
|
}
|
|
587
859
|
);
|
|
588
860
|
figma.ui.postMessage({ type: 'sync-status', message: 'PR created!', success: true, url: prUrl });
|
|
589
|
-
} catch (e
|
|
590
|
-
figma.notify('Sync failed: ' + e
|
|
591
|
-
figma.ui.postMessage({ type: 'sync-status', message: 'Failed: ' + e
|
|
861
|
+
} catch (e) {
|
|
862
|
+
figma.notify('Sync failed: ' + errorMessage(e), { timeout: 2500 });
|
|
863
|
+
figma.ui.postMessage({ type: 'sync-status', message: 'Failed: ' + errorMessage(e), success: false });
|
|
592
864
|
}
|
|
593
865
|
}
|
|
594
866
|
|
|
595
867
|
if (msg.type === 'show-push') {
|
|
868
|
+
const showPushTier = await checkLicense();
|
|
869
|
+
if (showPushTier !== 'pro') {
|
|
870
|
+
figma.ui.postMessage({ type: 'show-view', view: 'upgrade' });
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
596
873
|
await loadConfig();
|
|
597
874
|
const hasPushToken = await figma.clientStorage.getAsync('github_token');
|
|
598
875
|
figma.ui.postMessage({ type: 'show-view', view: 'push' });
|
|
@@ -632,15 +909,20 @@ figma.ui.onmessage = async (msg: any) => {
|
|
|
632
909
|
}
|
|
633
910
|
rebuildColorIndex(TOKENS);
|
|
634
911
|
createOrUpdateStyles();
|
|
635
|
-
buildDesignSystemSinglePage();
|
|
636
|
-
figma.notify('✅
|
|
637
|
-
await
|
|
912
|
+
await buildDesignSystemSinglePage();
|
|
913
|
+
figma.notify('✅ Design system created');
|
|
914
|
+
await waitForFontsBeforeClose();
|
|
638
915
|
figma.closePlugin();
|
|
639
916
|
}
|
|
640
917
|
|
|
641
918
|
if (msg.type === 'resize') {
|
|
642
919
|
const h = typeof msg.height === 'number' ? Math.max(80, msg.height) : 400;
|
|
643
|
-
|
|
920
|
+
// Preserve the current panel width — different flows show the panel
|
|
921
|
+
// at different widths (320 for settings/configure, 360 for loading /
|
|
922
|
+
// preflight / sync). The UI passes its viewport width so we don't
|
|
923
|
+
// snap to a hardcoded value and cause a horizontal jump.
|
|
924
|
+
const w = typeof msg.width === 'number' ? Math.max(120, msg.width) : 320;
|
|
925
|
+
figma.ui.resize(w, h);
|
|
644
926
|
}
|
|
645
927
|
|
|
646
928
|
if (msg.type === 'show-settings') {
|
|
@@ -665,14 +947,14 @@ if (figma.codegen && typeof figma.codegen.on === 'function') {
|
|
|
665
947
|
// Initialize color index for codegen
|
|
666
948
|
rebuildColorIndex(TOKENS);
|
|
667
949
|
|
|
668
|
-
figma.codegen.on('generate', (event:
|
|
950
|
+
figma.codegen.on('generate', (event: { node: SceneNode }) => {
|
|
669
951
|
try {
|
|
670
952
|
const node = event.node;
|
|
671
953
|
const classes = tailwindForNode(node);
|
|
672
954
|
const code = 'className="' + classes + '"';
|
|
673
955
|
return [{ title: 'Tailwind CSS', code, language: 'PLAINTEXT' }];
|
|
674
|
-
} catch (e
|
|
675
|
-
return [{ title: 'Tailwind CSS', code: '// Unable to generate: ' + e
|
|
956
|
+
} catch (e) {
|
|
957
|
+
return [{ title: 'Tailwind CSS', code: '// Unable to generate: ' + errorMessage(e), language: 'PLAINTEXT' }];
|
|
676
958
|
}
|
|
677
959
|
});
|
|
678
960
|
}
|