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.
- 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 +145 -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/ui.html
ADDED
|
@@ -0,0 +1,1222 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<style>
|
|
5
|
+
* { box-sizing: border-box; }
|
|
6
|
+
body {
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
8
|
+
padding: 16px;
|
|
9
|
+
margin: 0;
|
|
10
|
+
background: #fff;
|
|
11
|
+
font-size: 12px;
|
|
12
|
+
}
|
|
13
|
+
h2 { margin: 0 0 16px 0; font-size: 14px; font-weight: 600; }
|
|
14
|
+
.field { margin-bottom: 12px; }
|
|
15
|
+
label { display: block; font-size: 11px; color: #666; margin-bottom: 4px; }
|
|
16
|
+
input, textarea, select {
|
|
17
|
+
width: 100%;
|
|
18
|
+
padding: 8px;
|
|
19
|
+
border: 1px solid #ddd;
|
|
20
|
+
border-radius: 4px;
|
|
21
|
+
font-size: 12px;
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
background: #fff;
|
|
24
|
+
}
|
|
25
|
+
input:focus, textarea:focus, select:focus {
|
|
26
|
+
outline: none;
|
|
27
|
+
border-color: #18a058;
|
|
28
|
+
}
|
|
29
|
+
textarea { resize: vertical; min-height: 60px; }
|
|
30
|
+
button {
|
|
31
|
+
width: 100%;
|
|
32
|
+
padding: 10px;
|
|
33
|
+
background: #18a058;
|
|
34
|
+
color: white;
|
|
35
|
+
border: none;
|
|
36
|
+
border-radius: 4px;
|
|
37
|
+
font-size: 12px;
|
|
38
|
+
font-weight: 500;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
margin-top: 8px;
|
|
41
|
+
}
|
|
42
|
+
button:hover { background: #16914e; }
|
|
43
|
+
button:disabled { background: #ccc; cursor: not-allowed; }
|
|
44
|
+
button.secondary {
|
|
45
|
+
background: #f5f5f5;
|
|
46
|
+
color: #333;
|
|
47
|
+
border: 1px solid #ddd;
|
|
48
|
+
}
|
|
49
|
+
button.secondary:hover { background: #e8e8e8; }
|
|
50
|
+
.status {
|
|
51
|
+
font-size: 11px;
|
|
52
|
+
color: #666;
|
|
53
|
+
margin-top: 12px;
|
|
54
|
+
padding: 8px;
|
|
55
|
+
border-radius: 4px;
|
|
56
|
+
display: none;
|
|
57
|
+
}
|
|
58
|
+
.status.show { display: block; }
|
|
59
|
+
.error { color: #d32f2f; background: #ffebee; }
|
|
60
|
+
.success { color: #18a058; background: #e8f5e9; }
|
|
61
|
+
.info { color: #1976d2; background: #e3f2fd; }
|
|
62
|
+
|
|
63
|
+
/* Config view */
|
|
64
|
+
#configView, #settingsView { display: none; }
|
|
65
|
+
.hint {
|
|
66
|
+
font-size: 10px;
|
|
67
|
+
color: #888;
|
|
68
|
+
margin-top: 4px;
|
|
69
|
+
line-height: 1.4;
|
|
70
|
+
}
|
|
71
|
+
.hint a { color: #18a058; text-decoration: none; }
|
|
72
|
+
.hint a:hover { text-decoration: underline; }
|
|
73
|
+
|
|
74
|
+
.status a {
|
|
75
|
+
color: inherit;
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.divider {
|
|
80
|
+
height: 1px;
|
|
81
|
+
background: #eee;
|
|
82
|
+
margin: 16px 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.section-title {
|
|
86
|
+
font-size: 11px;
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
color: #333;
|
|
89
|
+
margin-bottom: 12px;
|
|
90
|
+
text-transform: uppercase;
|
|
91
|
+
letter-spacing: 0.5px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.repo-display {
|
|
95
|
+
font-size: 11px;
|
|
96
|
+
color: #666;
|
|
97
|
+
padding: 8px;
|
|
98
|
+
background: #f9f9f9;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
margin-bottom: 12px;
|
|
101
|
+
}
|
|
102
|
+
.repo-display strong { color: #333; }
|
|
103
|
+
|
|
104
|
+
.settings-link {
|
|
105
|
+
font-size: 10px;
|
|
106
|
+
color: #18a058;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
text-decoration: underline;
|
|
109
|
+
margin-top: 4px;
|
|
110
|
+
display: inline-block;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Change selection styles */
|
|
114
|
+
.change-list {
|
|
115
|
+
max-height: 200px;
|
|
116
|
+
overflow-y: auto;
|
|
117
|
+
border: 1px solid #eee;
|
|
118
|
+
border-radius: 4px;
|
|
119
|
+
margin-bottom: 12px;
|
|
120
|
+
}
|
|
121
|
+
.change-item {
|
|
122
|
+
padding: 8px 12px;
|
|
123
|
+
border-bottom: 1px solid #eee;
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: flex-start;
|
|
126
|
+
gap: 8px;
|
|
127
|
+
}
|
|
128
|
+
.change-item:last-child { border-bottom: none; }
|
|
129
|
+
.change-item input[type="checkbox"] {
|
|
130
|
+
margin-top: 2px;
|
|
131
|
+
width: auto;
|
|
132
|
+
}
|
|
133
|
+
.change-item label {
|
|
134
|
+
flex: 1;
|
|
135
|
+
margin-bottom: 0;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
}
|
|
138
|
+
.change-item .change-name {
|
|
139
|
+
font-weight: 500;
|
|
140
|
+
color: #333;
|
|
141
|
+
}
|
|
142
|
+
.change-item .change-details {
|
|
143
|
+
font-size: 10px;
|
|
144
|
+
color: #666;
|
|
145
|
+
margin-top: 2px;
|
|
146
|
+
}
|
|
147
|
+
.change-item .change-badge {
|
|
148
|
+
font-size: 9px;
|
|
149
|
+
padding: 2px 6px;
|
|
150
|
+
border-radius: 10px;
|
|
151
|
+
background: #e3f2fd;
|
|
152
|
+
color: #1976d2;
|
|
153
|
+
}
|
|
154
|
+
.change-item .change-badge.token { background: #e8f5e9; color: #18a058; }
|
|
155
|
+
.change-item .change-badge.component { background: #fff3e0; color: #f57c00; }
|
|
156
|
+
.no-changes {
|
|
157
|
+
padding: 16px;
|
|
158
|
+
text-align: center;
|
|
159
|
+
color: #666;
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
}
|
|
162
|
+
.select-actions {
|
|
163
|
+
display: flex;
|
|
164
|
+
gap: 8px;
|
|
165
|
+
margin-bottom: 8px;
|
|
166
|
+
}
|
|
167
|
+
.select-actions a {
|
|
168
|
+
font-size: 10px;
|
|
169
|
+
color: #18a058;
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
text-decoration: underline;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Offline view */
|
|
175
|
+
.offline-icon {
|
|
176
|
+
font-size: 36px;
|
|
177
|
+
text-align: center;
|
|
178
|
+
margin-bottom: 12px;
|
|
179
|
+
}
|
|
180
|
+
.command-block {
|
|
181
|
+
background: #1e1e1e;
|
|
182
|
+
color: #d4d4d4;
|
|
183
|
+
border-radius: 6px;
|
|
184
|
+
padding: 12px 14px;
|
|
185
|
+
margin: 12px 0;
|
|
186
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
187
|
+
font-size: 12px;
|
|
188
|
+
letter-spacing: 0.2px;
|
|
189
|
+
}
|
|
190
|
+
.command-block .prompt {
|
|
191
|
+
color: #18a058;
|
|
192
|
+
margin-right: 6px;
|
|
193
|
+
user-select: none;
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
196
|
+
</head>
|
|
197
|
+
<body>
|
|
198
|
+
<!-- Push View -->
|
|
199
|
+
<div id="pushView">
|
|
200
|
+
<h2>Push to Code</h2>
|
|
201
|
+
|
|
202
|
+
<div id="repoInfo" class="repo-display" style="display:none;">
|
|
203
|
+
Target: <strong id="repoLabel"></strong>
|
|
204
|
+
<span class="settings-link" onclick="showSettings()">Change</span>
|
|
205
|
+
</div>
|
|
206
|
+
<div id="tokenSourceInfoPush" class="repo-display" style="display:none;padding:6px 8px;">
|
|
207
|
+
Last scan source: <strong id="tokenSourceLabelPush"></strong><br>
|
|
208
|
+
Configured mode: <strong id="configuredTokenSourceModePush">auto</strong>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div class="field">
|
|
212
|
+
<label>Commit Message</label>
|
|
213
|
+
<input type="text" id="commitMessage" placeholder="Update design tokens" value="Update design tokens">
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div class="field">
|
|
217
|
+
<label>PR Description (optional)</label>
|
|
218
|
+
<textarea id="prDescription" placeholder="Describe what changed..."></textarea>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<button id="pushBtn">Create Pull Request</button>
|
|
222
|
+
|
|
223
|
+
<div id="status" class="status"></div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- Config View (legacy - token only) -->
|
|
227
|
+
<div id="configView">
|
|
228
|
+
<h2>Configure GitHub</h2>
|
|
229
|
+
|
|
230
|
+
<div class="field">
|
|
231
|
+
<label>Personal Access Token</label>
|
|
232
|
+
<input type="password" id="githubToken" placeholder="ghp_xxxxxxxxxxxx">
|
|
233
|
+
<p class="hint">
|
|
234
|
+
Create at: GitHub → Settings → Developer settings →
|
|
235
|
+
<a href="https://github.com/settings/tokens?type=beta" target="_blank">Fine-grained tokens</a><br>
|
|
236
|
+
Permissions: Contents (read/write), Pull requests (read/write)
|
|
237
|
+
</p>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<button id="saveBtn">Save Token</button>
|
|
241
|
+
|
|
242
|
+
<div id="configStatus" class="status"></div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Settings View (full configuration) -->
|
|
246
|
+
<div id="settingsView">
|
|
247
|
+
<h2>Project Settings</h2>
|
|
248
|
+
|
|
249
|
+
<div class="section-title">Repository</div>
|
|
250
|
+
|
|
251
|
+
<div id="tokenSourceInfoSettings" class="repo-display" style="display:none;padding:6px 8px;">
|
|
252
|
+
Last scan source: <strong id="tokenSourceLabelSettings"></strong><br>
|
|
253
|
+
Configured mode: <strong id="configuredTokenSourceModeSettings">auto</strong>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div class="field">
|
|
257
|
+
<label>GitHub Owner / Org</label>
|
|
258
|
+
<input type="text" id="settingsOwner" placeholder="e.g., my-org or my-username">
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div class="field">
|
|
262
|
+
<label>Repository Name</label>
|
|
263
|
+
<input type="text" id="settingsRepo" placeholder="e.g., my-project">
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="field">
|
|
267
|
+
<label>Base Branch</label>
|
|
268
|
+
<input type="text" id="settingsBranch" placeholder="main" value="main">
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div class="field">
|
|
272
|
+
<label>Token Source Mode</label>
|
|
273
|
+
<select id="settingsTokenSourceMode">
|
|
274
|
+
<option value="auto">auto (prefer CSS)</option>
|
|
275
|
+
<option value="css">css (force CSS only)</option>
|
|
276
|
+
<option value="dtcg">dtcg (force DTCG only)</option>
|
|
277
|
+
</select>
|
|
278
|
+
<p class="hint">`auto` prefers CSS and falls back to DTCG, then embedded tokens.</p>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div class="field" id="tokenPathField">
|
|
282
|
+
<label>DTCG Token Path</label>
|
|
283
|
+
<input type="text" id="settingsTokenPath" placeholder="design-tokens/tokens.dtcg.json" value="design-tokens/tokens.dtcg.json">
|
|
284
|
+
<p class="hint">Path to the DTCG token file (used in dtcg mode or as fallback in auto mode)</p>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div class="field" id="cssTokenPathField">
|
|
288
|
+
<label>CSS Token Path (optional)</label>
|
|
289
|
+
<input type="text" id="settingsCssTokenPath" placeholder="src/app/tokens.css">
|
|
290
|
+
<p class="hint">Overrides CSS auto-discovery when set.</p>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div class="field" id="syncDtcgOnPushField">
|
|
294
|
+
<label style="display:flex;align-items:center;gap:8px;">
|
|
295
|
+
<input type="checkbox" id="settingsSyncDtcgOnPush">
|
|
296
|
+
Also update DTCG on Push to Code
|
|
297
|
+
</label>
|
|
298
|
+
<p class="hint">When enabled, the plugin also commits <code>tokens.dtcg.json</code> as a generated artifact.</p>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div class="field">
|
|
302
|
+
<label style="display:flex;align-items:center;gap:8px;">
|
|
303
|
+
<input type="checkbox" id="settingsAllowNewTokensFromFigma">
|
|
304
|
+
Allow New Tokens from Figma
|
|
305
|
+
</label>
|
|
306
|
+
<p class="hint">Disabled by default. When enabled, new token keys can be added to code on push.</p>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div class="field">
|
|
310
|
+
<label>New Token Prefixes (optional)</label>
|
|
311
|
+
<input type="text" id="settingsNewTokenPrefixes" placeholder="chart-, brand-">
|
|
312
|
+
<p class="hint">Comma-separated. If empty, all new keys are allowed. If set, only matching prefixes are allowed.</p>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<div class="divider"></div>
|
|
316
|
+
|
|
317
|
+
<div class="section-title">Authentication</div>
|
|
318
|
+
|
|
319
|
+
<div class="field">
|
|
320
|
+
<label>GitHub Personal Access Token</label>
|
|
321
|
+
<input type="password" id="settingsToken" placeholder="ghp_xxxxxxxxxxxx">
|
|
322
|
+
<p class="hint">
|
|
323
|
+
Create at: <a href="https://github.com/settings/tokens?type=beta" target="_blank">Fine-grained tokens</a><br>
|
|
324
|
+
Permissions: Contents (read/write), Pull requests (read/write)
|
|
325
|
+
</p>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div class="divider"></div>
|
|
329
|
+
|
|
330
|
+
<div class="section-title">Display (Optional)</div>
|
|
331
|
+
|
|
332
|
+
<div class="field">
|
|
333
|
+
<label>Project Name</label>
|
|
334
|
+
<input type="text" id="settingsProjectName" placeholder="e.g., My Project">
|
|
335
|
+
<p class="hint">Used in PR descriptions. Defaults to owner/repo if empty.</p>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<div class="divider"></div>
|
|
339
|
+
|
|
340
|
+
<div class="section-title">License</div>
|
|
341
|
+
|
|
342
|
+
<div id="licenseStatusRow" class="field" style="display:none;">
|
|
343
|
+
<label>Current Plan</label>
|
|
344
|
+
<div id="licenseStatusBadge" style="font-size:11px;padding:5px 8px;border-radius:4px;background:#f5f5f5;color:#666;"></div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div class="field">
|
|
348
|
+
<label>License Key</label>
|
|
349
|
+
<input type="text" id="settingsLicenseKey" placeholder="INK-xxxx-xxxx-xxxx">
|
|
350
|
+
<p class="hint">Unlocks GitHub PR and two-way sync. Leave blank to keep existing key.</p>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<button id="settingsSaveBtn">Save Settings</button>
|
|
354
|
+
|
|
355
|
+
<div id="settingsStatus" class="status"></div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<!-- Offline View -->
|
|
359
|
+
<div id="offlineView" style="display:none;">
|
|
360
|
+
<div class="offline-icon">⚡</div>
|
|
361
|
+
<h2 style="text-align:center;margin:0 0 8px;">Dev server not running</h2>
|
|
362
|
+
<p style="font-size:12px;color:#666;text-align:center;margin:0 0 12px;line-height:1.5;">
|
|
363
|
+
The plugin needs your local dev server to scan components.
|
|
364
|
+
</p>
|
|
365
|
+
|
|
366
|
+
<div class="command-block">
|
|
367
|
+
<span class="prompt">$</span>pnpm figma:dev
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<p style="font-size:11px;color:#888;margin:0 0 16px;line-height:1.5;">
|
|
371
|
+
This starts Next.js and the component scanner. Once it's running, click Retry.
|
|
372
|
+
</p>
|
|
373
|
+
|
|
374
|
+
<button id="retryBtn">Retry</button>
|
|
375
|
+
<div id="retryStatus" class="status"></div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<!-- Upgrade View -->
|
|
379
|
+
<div id="upgradeView" style="display:none;">
|
|
380
|
+
<h2>Pro Feature</h2>
|
|
381
|
+
<p style="font-size:12px;color:#666;margin-bottom:16px;">
|
|
382
|
+
Push to Code requires an Inkhouse Pro license.<br>
|
|
383
|
+
Enter your license key in Settings to activate.
|
|
384
|
+
</p>
|
|
385
|
+
<button onclick="parent.postMessage({ pluginMessage: { type: 'show-settings' } }, '*')">Open Settings</button>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<!-- Overview View (Pro landing screen) -->
|
|
389
|
+
<div id="overviewView" style="display:none;">
|
|
390
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
|
|
391
|
+
<h2 style="margin:0;">Push to Code</h2>
|
|
392
|
+
<span style="font-size:10px;padding:2px 8px;border-radius:10px;background:#e8f5e9;color:#18a058;font-weight:600;letter-spacing:0.5px;">PRO</span>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<button id="overviewPushBtn">Push Tokens to Code</button>
|
|
396
|
+
<button id="overviewSyncBtn" class="secondary" style="margin-top:4px;">Detect & Sync Changes</button>
|
|
397
|
+
<div id="overviewStatus" class="status"></div>
|
|
398
|
+
|
|
399
|
+
<div class="divider"></div>
|
|
400
|
+
|
|
401
|
+
<span class="settings-link" onclick="showSettings()">⚙ Project Settings</span>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<!-- Sync Selection View -->
|
|
405
|
+
<div id="syncView" style="display:none;">
|
|
406
|
+
<h2>Sync to Code</h2>
|
|
407
|
+
|
|
408
|
+
<div id="repoInfoSync" class="repo-display" style="display:none;">
|
|
409
|
+
Target: <strong id="repoLabelSync"></strong>
|
|
410
|
+
</div>
|
|
411
|
+
<div id="tokenSourceInfoSync" class="repo-display" style="display:none;padding:6px 8px;">
|
|
412
|
+
Last scan source: <strong id="tokenSourceLabelSync"></strong><br>
|
|
413
|
+
Configured mode: <strong id="configuredTokenSourceModeSync">auto</strong>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<p style="font-size: 11px; color: #666; margin-bottom: 12px;">
|
|
417
|
+
Select what to include in the pull request:
|
|
418
|
+
</p>
|
|
419
|
+
|
|
420
|
+
<div class="select-actions">
|
|
421
|
+
<a onclick="selectAllChanges()">Select all</a>
|
|
422
|
+
<a onclick="selectNoneChanges()">Select none</a>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<div id="changeList" class="change-list">
|
|
426
|
+
<div class="no-changes">Detecting changes...</div>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<div class="field">
|
|
430
|
+
<label>Commit Message</label>
|
|
431
|
+
<input type="text" id="syncCommitMessage" placeholder="Update design tokens and components" value="Update from Figma">
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<div class="field">
|
|
435
|
+
<label>PR Description (optional)</label>
|
|
436
|
+
<textarea id="syncPrDescription" placeholder="Describe what changed..."></textarea>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<button id="syncBtn" onclick="return window.__syncClick(event)">Create Pull Request</button>
|
|
440
|
+
<button class="secondary" onclick="parent.postMessage({ pluginMessage: { type: 'show-overview' } }, '*')" style="margin-top: 4px;">Back</button>
|
|
441
|
+
|
|
442
|
+
<div id="syncStatus" class="status"></div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<script>
|
|
446
|
+
// Elements
|
|
447
|
+
var pushView = document.getElementById('pushView');
|
|
448
|
+
var configView = document.getElementById('configView');
|
|
449
|
+
var settingsView = document.getElementById('settingsView');
|
|
450
|
+
var syncView = document.getElementById('syncView');
|
|
451
|
+
var upgradeView = document.getElementById('upgradeView');
|
|
452
|
+
var overviewView = document.getElementById('overviewView');
|
|
453
|
+
var offlineView = document.getElementById('offlineView');
|
|
454
|
+
var retryBtn = document.getElementById('retryBtn');
|
|
455
|
+
var retryStatusEl = document.getElementById('retryStatus');
|
|
456
|
+
var overviewPushBtn = document.getElementById('overviewPushBtn');
|
|
457
|
+
var overviewSyncBtn = document.getElementById('overviewSyncBtn');
|
|
458
|
+
var overviewStatusEl = document.getElementById('overviewStatus');
|
|
459
|
+
var pushBtn = document.getElementById('pushBtn');
|
|
460
|
+
var saveBtn = document.getElementById('saveBtn');
|
|
461
|
+
var settingsSaveBtn = document.getElementById('settingsSaveBtn');
|
|
462
|
+
var syncBtn = document.getElementById('syncBtn');
|
|
463
|
+
var statusEl = document.getElementById('status');
|
|
464
|
+
var configStatusEl = document.getElementById('configStatus');
|
|
465
|
+
var settingsStatusEl = document.getElementById('settingsStatus');
|
|
466
|
+
var syncStatusEl = document.getElementById('syncStatus');
|
|
467
|
+
var commitInput = document.getElementById('commitMessage');
|
|
468
|
+
var descInput = document.getElementById('prDescription');
|
|
469
|
+
var tokenInput = document.getElementById('githubToken');
|
|
470
|
+
var repoInfo = document.getElementById('repoInfo');
|
|
471
|
+
var repoLabel = document.getElementById('repoLabel');
|
|
472
|
+
var tokenSourceInfoPush = document.getElementById('tokenSourceInfoPush');
|
|
473
|
+
var tokenSourceLabelPush = document.getElementById('tokenSourceLabelPush');
|
|
474
|
+
var configuredTokenSourceModePush = document.getElementById('configuredTokenSourceModePush');
|
|
475
|
+
var tokenSourceInfoSettings = document.getElementById('tokenSourceInfoSettings');
|
|
476
|
+
var tokenSourceLabelSettings = document.getElementById('tokenSourceLabelSettings');
|
|
477
|
+
var configuredTokenSourceModeSettings = document.getElementById('configuredTokenSourceModeSettings');
|
|
478
|
+
var changeListEl = document.getElementById('changeList');
|
|
479
|
+
var repoInfoSync = document.getElementById('repoInfoSync');
|
|
480
|
+
var repoLabelSync = document.getElementById('repoLabelSync');
|
|
481
|
+
var tokenSourceInfoSync = document.getElementById('tokenSourceInfoSync');
|
|
482
|
+
var tokenSourceLabelSync = document.getElementById('tokenSourceLabelSync');
|
|
483
|
+
var configuredTokenSourceModeSync = document.getElementById('configuredTokenSourceModeSync');
|
|
484
|
+
var syncCommitInput = document.getElementById('syncCommitMessage');
|
|
485
|
+
var syncDescInput = document.getElementById('syncPrDescription');
|
|
486
|
+
var syncRequestWatchdog = null;
|
|
487
|
+
var syncStatusErrorLockUntil = 0;
|
|
488
|
+
window.__syncReady = false;
|
|
489
|
+
window.__syncClickHandler = null;
|
|
490
|
+
window.__syncClick = function(event) {
|
|
491
|
+
if (window.__syncReady && typeof window.__syncClickHandler === 'function') {
|
|
492
|
+
return window.__syncClickHandler(event);
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
var fallbackStatus = document.getElementById('syncStatus');
|
|
496
|
+
var fallbackCommit = document.getElementById('syncCommitMessage');
|
|
497
|
+
var fallbackDesc = document.getElementById('syncPrDescription');
|
|
498
|
+
if (fallbackStatus) {
|
|
499
|
+
fallbackStatus.textContent = 'Submitting request (fallback)...';
|
|
500
|
+
fallbackStatus.className = 'status show info';
|
|
501
|
+
}
|
|
502
|
+
parent.postMessage({
|
|
503
|
+
pluginMessage: {
|
|
504
|
+
type: 'sync-to-github',
|
|
505
|
+
includeTokens: true,
|
|
506
|
+
components: [],
|
|
507
|
+
commitMessage: (fallbackCommit && fallbackCommit.value) ? fallbackCommit.value : 'Update from Figma',
|
|
508
|
+
prDescription: (fallbackDesc && fallbackDesc.value) ? fallbackDesc.value : '',
|
|
509
|
+
}
|
|
510
|
+
}, '*');
|
|
511
|
+
} catch (e) {
|
|
512
|
+
var fallbackErr = document.getElementById('syncStatus');
|
|
513
|
+
if (fallbackErr) {
|
|
514
|
+
fallbackErr.textContent = 'Failed: fallback click handler crashed';
|
|
515
|
+
fallbackErr.className = 'status show error';
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return false;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// Settings inputs
|
|
522
|
+
var settingsOwner = document.getElementById('settingsOwner');
|
|
523
|
+
var settingsRepo = document.getElementById('settingsRepo');
|
|
524
|
+
var settingsBranch = document.getElementById('settingsBranch');
|
|
525
|
+
var settingsTokenPath = document.getElementById('settingsTokenPath');
|
|
526
|
+
var tokenPathField = document.getElementById('tokenPathField');
|
|
527
|
+
var settingsTokenSourceMode = document.getElementById('settingsTokenSourceMode');
|
|
528
|
+
var settingsCssTokenPath = document.getElementById('settingsCssTokenPath');
|
|
529
|
+
var cssTokenPathField = document.getElementById('cssTokenPathField');
|
|
530
|
+
var settingsSyncDtcgOnPush = document.getElementById('settingsSyncDtcgOnPush');
|
|
531
|
+
var syncDtcgOnPushField = document.getElementById('syncDtcgOnPushField');
|
|
532
|
+
var settingsAllowNewTokensFromFigma = document.getElementById('settingsAllowNewTokensFromFigma');
|
|
533
|
+
var settingsNewTokenPrefixes = document.getElementById('settingsNewTokenPrefixes');
|
|
534
|
+
var settingsToken = document.getElementById('settingsToken');
|
|
535
|
+
var settingsProjectName = document.getElementById('settingsProjectName');
|
|
536
|
+
var settingsLicenseKey = document.getElementById('settingsLicenseKey');
|
|
537
|
+
|
|
538
|
+
// Current detected changes
|
|
539
|
+
var detectedChanges = { tokens: true, components: [] };
|
|
540
|
+
var lastTokenSourceInfo = null;
|
|
541
|
+
var configuredTokenSourceMode = 'auto';
|
|
542
|
+
|
|
543
|
+
function resizeToContent() {
|
|
544
|
+
// Wait for layout to settle before measuring
|
|
545
|
+
setTimeout(function() {
|
|
546
|
+
var h = document.body.scrollHeight;
|
|
547
|
+
parent.postMessage({ pluginMessage: { type: 'resize', height: h } }, '*');
|
|
548
|
+
}, 50);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function showSettings() {
|
|
552
|
+
parent.postMessage({ pluginMessage: { type: 'show-settings' } }, '*');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function showPushView() {
|
|
556
|
+
parent.postMessage({ pluginMessage: { type: 'show-push' } }, '*');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function setSyncStatus(message, kind, options) {
|
|
560
|
+
if (!syncStatusEl) return;
|
|
561
|
+
var now = Date.now();
|
|
562
|
+
var opts = options || {};
|
|
563
|
+
var force = opts.force === true;
|
|
564
|
+
if (!force && kind === 'info' && now < syncStatusErrorLockUntil) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
syncStatusEl.textContent = message || '';
|
|
568
|
+
syncStatusEl.className = 'status show ' + kind;
|
|
569
|
+
if (kind === 'error') {
|
|
570
|
+
syncStatusErrorLockUntil = now + 5000;
|
|
571
|
+
} else if (kind === 'success') {
|
|
572
|
+
syncStatusErrorLockUntil = 0;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function formatTokenSourceInfo(info) {
|
|
577
|
+
if (!info || !info.source) return null;
|
|
578
|
+
var modeLabel = info.mode || 'embedded';
|
|
579
|
+
return info.source + ' (' + modeLabel + ')';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function normalizeConfiguredMode(mode) {
|
|
583
|
+
if (mode === 'css' || mode === 'dtcg' || mode === 'auto') return mode;
|
|
584
|
+
return 'auto';
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function applyTokenSourceModeVisibility(mode) {
|
|
588
|
+
var normalized = normalizeConfiguredMode(mode);
|
|
589
|
+
if (normalized === 'css') {
|
|
590
|
+
tokenPathField.style.display = 'none';
|
|
591
|
+
cssTokenPathField.style.display = 'block';
|
|
592
|
+
syncDtcgOnPushField.style.display = 'block';
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (normalized === 'dtcg') {
|
|
596
|
+
tokenPathField.style.display = 'block';
|
|
597
|
+
cssTokenPathField.style.display = 'none';
|
|
598
|
+
syncDtcgOnPushField.style.display = 'none';
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
tokenPathField.style.display = 'block';
|
|
602
|
+
cssTokenPathField.style.display = 'block';
|
|
603
|
+
syncDtcgOnPushField.style.display = 'block';
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function setTokenSourceInfo(info) {
|
|
607
|
+
var label = formatTokenSourceInfo(info);
|
|
608
|
+
var configured = normalizeConfiguredMode(configuredTokenSourceMode);
|
|
609
|
+
var sourceLabel = label || 'Not scanned in this mode yet';
|
|
610
|
+
var display = 'block';
|
|
611
|
+
tokenSourceInfoPush.style.display = display;
|
|
612
|
+
tokenSourceInfoSettings.style.display = display;
|
|
613
|
+
tokenSourceInfoSync.style.display = display;
|
|
614
|
+
tokenSourceLabelPush.textContent = sourceLabel;
|
|
615
|
+
tokenSourceLabelSettings.textContent = sourceLabel;
|
|
616
|
+
tokenSourceLabelSync.textContent = sourceLabel;
|
|
617
|
+
configuredTokenSourceModePush.textContent = configured;
|
|
618
|
+
configuredTokenSourceModeSettings.textContent = configured;
|
|
619
|
+
configuredTokenSourceModeSync.textContent = configured;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function selectAllChanges() {
|
|
623
|
+
var checkboxes = changeListEl.querySelectorAll('input[type="checkbox"]');
|
|
624
|
+
checkboxes.forEach(function(cb) { cb.checked = true; });
|
|
625
|
+
updateSyncButton();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function selectNoneChanges() {
|
|
629
|
+
var checkboxes = changeListEl.querySelectorAll('input[type="checkbox"]');
|
|
630
|
+
checkboxes.forEach(function(cb) { cb.checked = false; });
|
|
631
|
+
updateSyncButton();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function updateSyncButton() {
|
|
635
|
+
if (!changeListEl || !syncBtn) return;
|
|
636
|
+
var allCheckboxes = changeListEl.querySelectorAll('input[type="checkbox"]');
|
|
637
|
+
// Keep submit available so clicks always give feedback (never an inert disabled button).
|
|
638
|
+
if (!allCheckboxes || allCheckboxes.length === 0) {
|
|
639
|
+
syncBtn.disabled = false;
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
syncBtn.disabled = false;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function renderChangeList(changes) {
|
|
646
|
+
detectedChanges = changes;
|
|
647
|
+
var html = '';
|
|
648
|
+
var hasTokenChanges = !(changes && changes.tokens === false);
|
|
649
|
+
|
|
650
|
+
// Token option
|
|
651
|
+
html += '<div class="change-item">' +
|
|
652
|
+
'<input type="checkbox" id="change-tokens" ' + (hasTokenChanges ? 'checked' : '') + ' ' + (hasTokenChanges ? '' : 'disabled') + ' onchange="updateSyncButton()">' +
|
|
653
|
+
'<label for="change-tokens">' +
|
|
654
|
+
'<span class="change-name">Design Tokens</span>' +
|
|
655
|
+
'<span class="change-badge token">tokens</span>' +
|
|
656
|
+
'<div class="change-details">' + (hasTokenChanges ? 'Colors, spacing, and other design variables' : 'Already in sync (no token changes detected)') + '</div>' +
|
|
657
|
+
'</label></div>';
|
|
658
|
+
|
|
659
|
+
// Show component changes
|
|
660
|
+
if (changes.components && changes.components.length > 0) {
|
|
661
|
+
for (var i = 0; i < changes.components.length; i++) {
|
|
662
|
+
var comp = changes.components[i];
|
|
663
|
+
var changeDesc = comp.changes.map(function(c) {
|
|
664
|
+
return c.property + ': ' + c.code + ' → ' + c.figma;
|
|
665
|
+
}).join(', ');
|
|
666
|
+
|
|
667
|
+
html += '<div class="change-item">' +
|
|
668
|
+
'<input type="checkbox" id="change-' + i + '" checked onchange="updateSyncButton()">' +
|
|
669
|
+
'<label for="change-' + i + '">' +
|
|
670
|
+
'<span class="change-name">' + comp.name + '</span>' +
|
|
671
|
+
'<span class="change-badge component">' + comp.type + '</span>' +
|
|
672
|
+
'<div class="change-details">' + changeDesc + '</div>' +
|
|
673
|
+
'</label></div>';
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
changeListEl.innerHTML = html;
|
|
678
|
+
updateSyncButton();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function getSelectedChanges() {
|
|
682
|
+
var tokenCheckbox = document.getElementById('change-tokens');
|
|
683
|
+
// Fallback: when token checkbox is missing, default to token sync enabled.
|
|
684
|
+
var includeTokens = tokenCheckbox ? !!tokenCheckbox.checked : true;
|
|
685
|
+
var selected = {
|
|
686
|
+
includeTokens: includeTokens,
|
|
687
|
+
components: []
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
if (detectedChanges.components) {
|
|
691
|
+
for (var i = 0; i < detectedChanges.components.length; i++) {
|
|
692
|
+
var checkbox = document.getElementById('change-' + i);
|
|
693
|
+
if (checkbox && checkbox.checked) {
|
|
694
|
+
selected.components.push(detectedChanges.components[i]);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return selected;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Notify code.js that the UI iframe is loaded and ready to receive messages
|
|
703
|
+
parent.postMessage({ pluginMessage: { type: 'ui-ready' } }, '*');
|
|
704
|
+
|
|
705
|
+
// Dev server fetch - runs in the UI iframe which has network access
|
|
706
|
+
// (code.js sandbox cannot make fetch calls)
|
|
707
|
+
var DEV_SERVER_ENDPOINTS = [
|
|
708
|
+
'http://localhost:4000/api/figma/scan-components',
|
|
709
|
+
'http://localhost:3000/api/figma/scan-components',
|
|
710
|
+
'http://localhost:5173/api/figma/scan-components',
|
|
711
|
+
];
|
|
712
|
+
|
|
713
|
+
var DEV_SERVER_TIMEOUT_MS = 15000;
|
|
714
|
+
|
|
715
|
+
async function fetchComponentDefs() {
|
|
716
|
+
for (var i = 0; i < DEV_SERVER_ENDPOINTS.length; i++) {
|
|
717
|
+
var url = DEV_SERVER_ENDPOINTS[i];
|
|
718
|
+
try {
|
|
719
|
+
var controller = new AbortController();
|
|
720
|
+
var timeoutId = setTimeout(function() { controller.abort(); }, DEV_SERVER_TIMEOUT_MS);
|
|
721
|
+
var response = await fetch(url, { method: 'GET', signal: controller.signal });
|
|
722
|
+
clearTimeout(timeoutId);
|
|
723
|
+
if (response.ok) {
|
|
724
|
+
return await response.json();
|
|
725
|
+
}
|
|
726
|
+
} catch (e) {
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function fetchPackFromCandidates(candidates) {
|
|
734
|
+
if (!candidates || !candidates.length) return null;
|
|
735
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
736
|
+
var url = candidates[i];
|
|
737
|
+
if (!url) continue;
|
|
738
|
+
try {
|
|
739
|
+
var controller = new AbortController();
|
|
740
|
+
var timeoutId = setTimeout(function() { controller.abort(); }, DEV_SERVER_TIMEOUT_MS);
|
|
741
|
+
var response = await fetch(url, { method: 'GET', signal: controller.signal });
|
|
742
|
+
clearTimeout(timeoutId);
|
|
743
|
+
if (response.ok) {
|
|
744
|
+
return { data: await response.json(), source: url };
|
|
745
|
+
}
|
|
746
|
+
} catch (e) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function patchCssFromCandidates(candidates, payload) {
|
|
754
|
+
if (!candidates || !candidates.length) return { error: 'no-candidates' };
|
|
755
|
+
var lastError = 'offline';
|
|
756
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
757
|
+
var url = candidates[i];
|
|
758
|
+
if (!url) continue;
|
|
759
|
+
try {
|
|
760
|
+
var controller = new AbortController();
|
|
761
|
+
var timeoutId = setTimeout(function() { controller.abort(); }, DEV_SERVER_TIMEOUT_MS);
|
|
762
|
+
var response = await fetch(url, {
|
|
763
|
+
method: 'POST',
|
|
764
|
+
headers: { 'Content-Type': 'application/json' },
|
|
765
|
+
body: JSON.stringify(payload || {}),
|
|
766
|
+
signal: controller.signal
|
|
767
|
+
});
|
|
768
|
+
clearTimeout(timeoutId);
|
|
769
|
+
if (!response.ok) {
|
|
770
|
+
var errBody = await response.json().catch(function() { return {}; });
|
|
771
|
+
lastError = errBody && errBody.message ? String(errBody.message) : ('HTTP ' + response.status);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
var data = await response.json().catch(function() { return {}; });
|
|
775
|
+
if (data && typeof data.patchedCss === 'string') {
|
|
776
|
+
return { patchedCss: data.patchedCss, source: url };
|
|
777
|
+
}
|
|
778
|
+
lastError = 'invalid-response';
|
|
779
|
+
} catch (e) {
|
|
780
|
+
lastError = (e && e.message) ? String(e.message) : 'offline';
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return { error: lastError };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Handle messages from plugin
|
|
788
|
+
window.onmessage = async function(event) {
|
|
789
|
+
var msg = event.data.pluginMessage;
|
|
790
|
+
if (!msg) return;
|
|
791
|
+
|
|
792
|
+
// UI-side image fetch (code.js sandbox has no network access)
|
|
793
|
+
if (msg.type === 'fetch-image') {
|
|
794
|
+
var imgBytes = null;
|
|
795
|
+
var svgText = null;
|
|
796
|
+
try {
|
|
797
|
+
var imgResp = await fetch(msg.url);
|
|
798
|
+
if (imgResp.ok) {
|
|
799
|
+
var contentType = imgResp.headers.get('content-type') || '';
|
|
800
|
+
var isSvg = contentType.includes('svg') || msg.url.toLowerCase().endsWith('.svg');
|
|
801
|
+
if (isSvg) {
|
|
802
|
+
svgText = await imgResp.text();
|
|
803
|
+
} else {
|
|
804
|
+
var buf = await imgResp.arrayBuffer();
|
|
805
|
+
imgBytes = Array.from(new Uint8Array(buf));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
} catch (e) { /* ignore */ }
|
|
809
|
+
parent.postMessage({ pluginMessage: { type: 'image-result', bytes: imgBytes, svgText: svgText, requestId: msg.requestId } }, '*');
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (msg.type === 'auto-start-detect') {
|
|
814
|
+
parent.postMessage({ pluginMessage: { type: 'detect-changes' } }, '*');
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// UI-side fetch for component definitions (code.js sandbox has no network)
|
|
819
|
+
if (msg.type === 'fetch-component-defs') {
|
|
820
|
+
var data = await fetchComponentDefs();
|
|
821
|
+
parent.postMessage({
|
|
822
|
+
pluginMessage: {
|
|
823
|
+
type: 'component-defs-result',
|
|
824
|
+
data: data,
|
|
825
|
+
requestId: msg.requestId
|
|
826
|
+
}
|
|
827
|
+
}, '*');
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (msg.type === 'fetch-pack') {
|
|
832
|
+
var packData = null;
|
|
833
|
+
var source = '';
|
|
834
|
+
var candidates = Array.isArray(msg.candidates) ? msg.candidates : [];
|
|
835
|
+
var result = await fetchPackFromCandidates(candidates);
|
|
836
|
+
if (result && result.data) {
|
|
837
|
+
packData = result.data;
|
|
838
|
+
source = result.source || 'url';
|
|
839
|
+
}
|
|
840
|
+
parent.postMessage({
|
|
841
|
+
pluginMessage: {
|
|
842
|
+
type: 'pack-result',
|
|
843
|
+
data: packData,
|
|
844
|
+
source: source,
|
|
845
|
+
requestId: msg.requestId
|
|
846
|
+
}
|
|
847
|
+
}, '*');
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (msg.type === 'patch-css') {
|
|
852
|
+
var patchCandidates = Array.isArray(msg.candidates) ? msg.candidates : [];
|
|
853
|
+
var patchPayload = msg.payload || {};
|
|
854
|
+
var patchResult = await patchCssFromCandidates(patchCandidates, patchPayload);
|
|
855
|
+
parent.postMessage({
|
|
856
|
+
pluginMessage: {
|
|
857
|
+
type: 'patch-css-result',
|
|
858
|
+
requestId: msg.requestId,
|
|
859
|
+
patchedCss: patchResult.patchedCss || null,
|
|
860
|
+
source: patchResult.source || '',
|
|
861
|
+
error: patchResult.error || null
|
|
862
|
+
}
|
|
863
|
+
}, '*');
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// UI-side license validation (code.js sandbox has no network)
|
|
868
|
+
if (msg.type === 'validate-license') {
|
|
869
|
+
var licenseResult = { tier: 'free', valid: false };
|
|
870
|
+
var validatePorts = ['4000', '3000', '5173'];
|
|
871
|
+
for (var vp = 0; vp < validatePorts.length; vp++) {
|
|
872
|
+
try {
|
|
873
|
+
var vUrl = 'http://localhost:' + validatePorts[vp] + '/api/plugin/validate?key=' + encodeURIComponent(msg.key);
|
|
874
|
+
var vResp = await fetch(vUrl, { method: 'GET' });
|
|
875
|
+
if (vResp.ok) {
|
|
876
|
+
licenseResult = await vResp.json();
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
} catch (e) {
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
parent.postMessage({ pluginMessage: { type: 'license-result', tier: licenseResult.tier, valid: licenseResult.valid } }, '*');
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// UI-side relay for GitHub API calls (code.js sandbox has no network)
|
|
888
|
+
if (msg.type === 'github-fetch') {
|
|
889
|
+
try {
|
|
890
|
+
var url = 'https://api.github.com' + msg.endpoint;
|
|
891
|
+
var fetchOpts = {
|
|
892
|
+
method: msg.method || 'GET',
|
|
893
|
+
headers: {
|
|
894
|
+
'Authorization': 'Bearer ' + msg.token,
|
|
895
|
+
'Accept': 'application/vnd.github+json',
|
|
896
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
897
|
+
'Content-Type': 'application/json'
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
if (msg.body) fetchOpts.body = msg.body;
|
|
901
|
+
var resp = await fetch(url, fetchOpts);
|
|
902
|
+
if (!resp.ok) {
|
|
903
|
+
var errBody = await resp.json().catch(function() { return {}; });
|
|
904
|
+
parent.postMessage({
|
|
905
|
+
pluginMessage: {
|
|
906
|
+
type: 'github-fetch-result',
|
|
907
|
+
requestId: msg.requestId,
|
|
908
|
+
data: { error: errBody.message || ('GitHub API error: ' + resp.status) }
|
|
909
|
+
}
|
|
910
|
+
}, '*');
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
var jsonData = await resp.json();
|
|
914
|
+
parent.postMessage({
|
|
915
|
+
pluginMessage: {
|
|
916
|
+
type: 'github-fetch-result',
|
|
917
|
+
requestId: msg.requestId,
|
|
918
|
+
data: jsonData
|
|
919
|
+
}
|
|
920
|
+
}, '*');
|
|
921
|
+
} catch (e) {
|
|
922
|
+
parent.postMessage({
|
|
923
|
+
pluginMessage: {
|
|
924
|
+
type: 'github-fetch-result',
|
|
925
|
+
requestId: msg.requestId,
|
|
926
|
+
data: { error: e.message || 'Network error' }
|
|
927
|
+
}
|
|
928
|
+
}, '*');
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (msg.type === 'show-view') {
|
|
934
|
+
pushView.style.display = msg.view === 'push' ? 'block' : 'none';
|
|
935
|
+
configView.style.display = msg.view === 'configure' ? 'block' : 'none';
|
|
936
|
+
settingsView.style.display = msg.view === 'settings' ? 'block' : 'none';
|
|
937
|
+
syncView.style.display = msg.view === 'sync' ? 'block' : 'none';
|
|
938
|
+
upgradeView.style.display = msg.view === 'upgrade' ? 'block' : 'none';
|
|
939
|
+
overviewView.style.display = msg.view === 'overview' ? 'block' : 'none';
|
|
940
|
+
offlineView.style.display = msg.view === 'offline' ? 'block' : 'none';
|
|
941
|
+
// Reset retry button state when navigating away from/to offline view
|
|
942
|
+
if (msg.view === 'offline') {
|
|
943
|
+
retryBtn.disabled = false;
|
|
944
|
+
retryBtn.textContent = 'Retry';
|
|
945
|
+
retryStatusEl.className = 'status';
|
|
946
|
+
}
|
|
947
|
+
if (msg.view === 'sync') {
|
|
948
|
+
syncBtn.disabled = false;
|
|
949
|
+
syncBtn.textContent = 'Create Pull Request';
|
|
950
|
+
}
|
|
951
|
+
resizeToContent();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (msg.type === 'license-key-loaded') {
|
|
955
|
+
if (msg.hasKey) {
|
|
956
|
+
settingsLicenseKey.placeholder = 'Key saved (enter new to replace)';
|
|
957
|
+
}
|
|
958
|
+
var badge = document.getElementById('licenseStatusBadge');
|
|
959
|
+
var row = document.getElementById('licenseStatusRow');
|
|
960
|
+
if (msg.tier === 'pro') {
|
|
961
|
+
badge.textContent = 'Pro — active';
|
|
962
|
+
badge.style.background = '#e8f5e9';
|
|
963
|
+
badge.style.color = '#18a058';
|
|
964
|
+
} else {
|
|
965
|
+
badge.textContent = msg.hasKey ? 'Free (key not yet validated)' : 'Free';
|
|
966
|
+
badge.style.background = '#f5f5f5';
|
|
967
|
+
badge.style.color = '#666';
|
|
968
|
+
}
|
|
969
|
+
row.style.display = 'block';
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (msg.type === 'config-loaded') {
|
|
973
|
+
var config = msg.config || {};
|
|
974
|
+
settingsOwner.value = config.owner || '';
|
|
975
|
+
settingsRepo.value = config.repo || '';
|
|
976
|
+
settingsBranch.value = config.baseBranch || 'main';
|
|
977
|
+
settingsTokenPath.value = config.tokenPath || 'design-tokens/tokens.dtcg.json';
|
|
978
|
+
var tokenSourceMode = normalizeConfiguredMode(config.tokenSourceMode);
|
|
979
|
+
settingsTokenSourceMode.value = tokenSourceMode;
|
|
980
|
+
settingsCssTokenPath.value = config.cssTokenPath || '';
|
|
981
|
+
settingsSyncDtcgOnPush.checked = config.syncDtcgOnPush === true;
|
|
982
|
+
settingsAllowNewTokensFromFigma.checked = config.allowNewTokensFromFigma === true;
|
|
983
|
+
settingsNewTokenPrefixes.value = Array.isArray(config.newTokenPrefixes) ? config.newTokenPrefixes.join(', ') : '';
|
|
984
|
+
settingsProjectName.value = config.projectName || '';
|
|
985
|
+
configuredTokenSourceMode = tokenSourceMode;
|
|
986
|
+
applyTokenSourceModeVisibility(tokenSourceMode);
|
|
987
|
+
setTokenSourceInfo(lastTokenSourceInfo);
|
|
988
|
+
|
|
989
|
+
// Update push view repo info
|
|
990
|
+
if (config.owner && config.repo) {
|
|
991
|
+
repoLabel.textContent = config.owner + '/' + config.repo;
|
|
992
|
+
repoInfo.style.display = 'block';
|
|
993
|
+
repoLabelSync.textContent = config.owner + '/' + config.repo;
|
|
994
|
+
repoInfoSync.style.display = 'block';
|
|
995
|
+
} else {
|
|
996
|
+
repoInfo.style.display = 'none';
|
|
997
|
+
repoInfoSync.style.display = 'none';
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Update token placeholder
|
|
1001
|
+
if (msg.hasToken) {
|
|
1002
|
+
settingsToken.placeholder = 'Token saved (enter new to replace)';
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (msg.type === 'token-source-info') {
|
|
1007
|
+
lastTokenSourceInfo = msg.info || null;
|
|
1008
|
+
setTokenSourceInfo(lastTokenSourceInfo);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (msg.type === 'pack-load-error') {
|
|
1012
|
+
settingsStatusEl.textContent = msg.message || 'Dev server not reachable / pack not found.';
|
|
1013
|
+
settingsStatusEl.className = 'status show error';
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (msg.type === 'changes-detected') {
|
|
1017
|
+
renderChangeList(msg.changes || { tokens: true, components: [] });
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (msg.type === 'sync-status') {
|
|
1021
|
+
if (syncRequestWatchdog) {
|
|
1022
|
+
clearTimeout(syncRequestWatchdog);
|
|
1023
|
+
syncRequestWatchdog = null;
|
|
1024
|
+
}
|
|
1025
|
+
setSyncStatus(
|
|
1026
|
+
msg.message,
|
|
1027
|
+
(msg.success ? 'success' : msg.success === false ? 'error' : 'info')
|
|
1028
|
+
);
|
|
1029
|
+
syncBtn.disabled = false;
|
|
1030
|
+
syncBtn.textContent = 'Create Pull Request';
|
|
1031
|
+
|
|
1032
|
+
if (msg.url) {
|
|
1033
|
+
syncStatusEl.innerHTML = 'PR created! <a href="' + msg.url + '" target="_blank">View PR →</a>';
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (msg.type === 'overview-status') {
|
|
1038
|
+
overviewStatusEl.textContent = msg.message || '';
|
|
1039
|
+
overviewStatusEl.className = 'status show ' + (msg.success ? 'success' : msg.success === false ? 'error' : 'info');
|
|
1040
|
+
overviewPushBtn.disabled = false;
|
|
1041
|
+
overviewSyncBtn.disabled = false;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (msg.type === 'push-status') {
|
|
1045
|
+
statusEl.textContent = msg.message;
|
|
1046
|
+
statusEl.className = 'status show ' + (msg.success ? 'success' : msg.success === false ? 'error' : 'info');
|
|
1047
|
+
pushBtn.disabled = false;
|
|
1048
|
+
|
|
1049
|
+
if (msg.url) {
|
|
1050
|
+
statusEl.innerHTML = 'PR created! <a href="' + msg.url + '" target="_blank">View PR →</a>';
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (msg.type === 'config-status') {
|
|
1055
|
+
configStatusEl.textContent = msg.message;
|
|
1056
|
+
configStatusEl.className = 'status show ' + (msg.success ? 'success' : 'error');
|
|
1057
|
+
saveBtn.disabled = false;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (msg.type === 'settings-status') {
|
|
1061
|
+
settingsStatusEl.textContent = msg.message;
|
|
1062
|
+
settingsStatusEl.className = 'status show ' + (msg.success ? 'success' : 'error');
|
|
1063
|
+
settingsSaveBtn.disabled = false;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (msg.type === 'token-loaded') {
|
|
1067
|
+
if (msg.hasToken) {
|
|
1068
|
+
tokenInput.value = '';
|
|
1069
|
+
tokenInput.placeholder = 'Token saved (enter new to replace)';
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
function runOverviewDetect(triggerBtn) {
|
|
1075
|
+
overviewPushBtn.disabled = true;
|
|
1076
|
+
overviewSyncBtn.disabled = true;
|
|
1077
|
+
if (overviewStatusEl) {
|
|
1078
|
+
overviewStatusEl.textContent = 'Detecting changes...';
|
|
1079
|
+
overviewStatusEl.className = 'status show info';
|
|
1080
|
+
}
|
|
1081
|
+
if (triggerBtn) triggerBtn.disabled = true;
|
|
1082
|
+
parent.postMessage({ pluginMessage: { type: 'detect-changes' } }, '*');
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Overview buttons
|
|
1086
|
+
overviewPushBtn.onclick = function() {
|
|
1087
|
+
runOverviewDetect(overviewPushBtn);
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
overviewSyncBtn.onclick = function() {
|
|
1091
|
+
runOverviewDetect(overviewSyncBtn);
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
// Push button - now opens sync view with change detection
|
|
1095
|
+
pushBtn.onclick = function() {
|
|
1096
|
+
pushBtn.disabled = true;
|
|
1097
|
+
statusEl.textContent = 'Detecting changes...';
|
|
1098
|
+
statusEl.className = 'status show info';
|
|
1099
|
+
|
|
1100
|
+
parent.postMessage({
|
|
1101
|
+
pluginMessage: {
|
|
1102
|
+
type: 'detect-changes'
|
|
1103
|
+
}
|
|
1104
|
+
}, '*');
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
// Sync button - creates PR with selected changes
|
|
1108
|
+
window.__syncClickHandler = function() {
|
|
1109
|
+
try {
|
|
1110
|
+
syncStatusErrorLockUntil = 0;
|
|
1111
|
+
setSyncStatus('Submitting request...', 'info', { force: true });
|
|
1112
|
+
var selected = getSelectedChanges();
|
|
1113
|
+
if (!selected.includeTokens && selected.components.length === 0) {
|
|
1114
|
+
setSyncStatus('Please select at least one item', 'error', { force: true });
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
syncBtn.disabled = true;
|
|
1119
|
+
syncBtn.textContent = 'Submitting...';
|
|
1120
|
+
|
|
1121
|
+
parent.postMessage({
|
|
1122
|
+
pluginMessage: {
|
|
1123
|
+
type: 'sync-to-github',
|
|
1124
|
+
includeTokens: selected.includeTokens,
|
|
1125
|
+
components: selected.components,
|
|
1126
|
+
commitMessage: syncCommitInput.value || 'Update from Figma',
|
|
1127
|
+
prDescription: syncDescInput.value || ''
|
|
1128
|
+
}
|
|
1129
|
+
}, '*');
|
|
1130
|
+
syncRequestWatchdog = setTimeout(function() {
|
|
1131
|
+
if (!syncStatusEl) return;
|
|
1132
|
+
setSyncStatus('No response from plugin runtime. Please reload the plugin and try again.', 'error', { force: true });
|
|
1133
|
+
syncBtn.disabled = false;
|
|
1134
|
+
syncBtn.textContent = 'Create Pull Request';
|
|
1135
|
+
syncRequestWatchdog = null;
|
|
1136
|
+
}, 2000);
|
|
1137
|
+
} catch (e) {
|
|
1138
|
+
var message = (e && e.message) ? e.message : 'Unexpected UI error';
|
|
1139
|
+
setSyncStatus('Failed: ' + message, 'error', { force: true });
|
|
1140
|
+
syncBtn.disabled = false;
|
|
1141
|
+
syncBtn.textContent = 'Create Pull Request';
|
|
1142
|
+
}
|
|
1143
|
+
return false;
|
|
1144
|
+
};
|
|
1145
|
+
syncBtn.onclick = window.__syncClickHandler;
|
|
1146
|
+
window.__syncReady = true;
|
|
1147
|
+
|
|
1148
|
+
// Save button (legacy config view)
|
|
1149
|
+
saveBtn.onclick = function() {
|
|
1150
|
+
if (!tokenInput.value.trim()) {
|
|
1151
|
+
configStatusEl.textContent = 'Please enter a token';
|
|
1152
|
+
configStatusEl.className = 'status show error';
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
saveBtn.disabled = true;
|
|
1157
|
+
configStatusEl.textContent = 'Saving...';
|
|
1158
|
+
configStatusEl.className = 'status show info';
|
|
1159
|
+
|
|
1160
|
+
parent.postMessage({
|
|
1161
|
+
pluginMessage: {
|
|
1162
|
+
type: 'save-github-token',
|
|
1163
|
+
token: tokenInput.value.trim()
|
|
1164
|
+
}
|
|
1165
|
+
}, '*');
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
// Offline view retry button
|
|
1169
|
+
retryBtn.onclick = function() {
|
|
1170
|
+
retryBtn.disabled = true;
|
|
1171
|
+
retryBtn.textContent = 'Retrying...';
|
|
1172
|
+
retryStatusEl.textContent = 'Connecting to dev server...';
|
|
1173
|
+
retryStatusEl.className = 'status show info';
|
|
1174
|
+
parent.postMessage({ pluginMessage: { type: 'retry-pack-load' } }, '*');
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
// Settings save button
|
|
1178
|
+
settingsSaveBtn.onclick = function() {
|
|
1179
|
+
var owner = settingsOwner.value.trim();
|
|
1180
|
+
var repo = settingsRepo.value.trim();
|
|
1181
|
+
|
|
1182
|
+
var hasLicenseKey = settingsLicenseKey.value.trim().length > 0;
|
|
1183
|
+
if (!hasLicenseKey && (!owner || !repo)) {
|
|
1184
|
+
settingsStatusEl.textContent = 'Owner and Repository are required';
|
|
1185
|
+
settingsStatusEl.className = 'status show error';
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
settingsSaveBtn.disabled = true;
|
|
1190
|
+
settingsStatusEl.textContent = 'Saving...';
|
|
1191
|
+
settingsStatusEl.className = 'status show info';
|
|
1192
|
+
|
|
1193
|
+
parent.postMessage({
|
|
1194
|
+
pluginMessage: {
|
|
1195
|
+
type: 'save-settings',
|
|
1196
|
+
owner: owner,
|
|
1197
|
+
repo: repo,
|
|
1198
|
+
baseBranch: settingsBranch.value.trim() || 'main',
|
|
1199
|
+
tokenPath: settingsTokenPath.value.trim() || 'design-tokens/tokens.dtcg.json',
|
|
1200
|
+
tokenSourceMode: settingsTokenSourceMode.value || 'auto',
|
|
1201
|
+
cssTokenPath: settingsCssTokenPath.value.trim(),
|
|
1202
|
+
syncDtcgOnPush: settingsSyncDtcgOnPush.checked === true,
|
|
1203
|
+
allowNewTokensFromFigma: settingsAllowNewTokensFromFigma.checked === true,
|
|
1204
|
+
newTokenPrefixes: settingsNewTokenPrefixes.value
|
|
1205
|
+
.split(',')
|
|
1206
|
+
.map(function(v) { return v.trim(); })
|
|
1207
|
+
.filter(function(v) { return v.length > 0; }),
|
|
1208
|
+
projectName: settingsProjectName.value.trim(),
|
|
1209
|
+
token: settingsToken.value.trim() || null,
|
|
1210
|
+
licenseKey: settingsLicenseKey.value.trim() || null
|
|
1211
|
+
}
|
|
1212
|
+
}, '*');
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
settingsTokenSourceMode.onchange = function() {
|
|
1216
|
+
configuredTokenSourceMode = normalizeConfiguredMode(settingsTokenSourceMode.value);
|
|
1217
|
+
applyTokenSourceModeVisibility(configuredTokenSourceMode);
|
|
1218
|
+
setTokenSourceInfo(lastTokenSourceInfo);
|
|
1219
|
+
};
|
|
1220
|
+
</script>
|
|
1221
|
+
</body>
|
|
1222
|
+
</html>
|