@lovision/plugin-dev 1.0.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 +49 -0
- package/dist/build.d.ts +16 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +108 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +123 -0
- package/dist/cli.js.map +1 -0
- package/dist/create-plugin.d.ts +19 -0
- package/dist/create-plugin.d.ts.map +1 -0
- package/dist/create-plugin.js +186 -0
- package/dist/create-plugin.js.map +1 -0
- package/dist/dev.d.ts +13 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +206 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/publish.d.ts +15 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +55 -0
- package/dist/publish.js.map +1 -0
- package/dist/shared.d.ts +93 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +436 -0
- package/dist/shared.js.map +1 -0
- package/dist/templates/ai-layout-assistant/README.md.template +24 -0
- package/dist/templates/ai-layout-assistant/eslint.config.mjs +19 -0
- package/dist/templates/ai-layout-assistant/manifest.json.template +38 -0
- package/dist/templates/ai-layout-assistant/package.json.template +18 -0
- package/dist/templates/ai-layout-assistant/src/main.ts.template +345 -0
- package/dist/templates/ai-layout-assistant/tsconfig.json +14 -0
- package/dist/templates/ai-layout-assistant/ui.html.template +114 -0
- package/dist/templates/asset-browser/README.md.template +24 -0
- package/dist/templates/asset-browser/eslint.config.mjs +19 -0
- package/dist/templates/asset-browser/manifest.json.template +29 -0
- package/dist/templates/asset-browser/package.json.template +18 -0
- package/dist/templates/asset-browser/src/main.ts.template +177 -0
- package/dist/templates/asset-browser/tsconfig.json +14 -0
- package/dist/templates/asset-browser/ui.html.template +137 -0
- package/dist/templates/base/README.md.template +34 -0
- package/dist/templates/base/eslint.config.mjs +19 -0
- package/dist/templates/base/manifest.json.template +22 -0
- package/dist/templates/base/package.json.template +18 -0
- package/dist/templates/base/src/main.ts.template +20 -0
- package/dist/templates/base/tsconfig.json +14 -0
- package/dist/templates/batch-layout-organizer/README.md.template +24 -0
- package/dist/templates/batch-layout-organizer/eslint.config.mjs +19 -0
- package/dist/templates/batch-layout-organizer/manifest.json.template +31 -0
- package/dist/templates/batch-layout-organizer/package.json.template +18 -0
- package/dist/templates/batch-layout-organizer/src/main.ts.template +324 -0
- package/dist/templates/batch-layout-organizer/tsconfig.json +14 -0
- package/dist/templates/batch-layout-organizer/ui.html.template +116 -0
- package/dist/templates/data-filler-full/README.md.template +32 -0
- package/dist/templates/data-filler-full/eslint.config.mjs +19 -0
- package/dist/templates/data-filler-full/manifest.json.template +31 -0
- package/dist/templates/data-filler-full/package.json.template +18 -0
- package/dist/templates/data-filler-full/src/main.ts.template +412 -0
- package/dist/templates/data-filler-full/tsconfig.json +14 -0
- package/dist/templates/data-filler-full/ui.html.template +221 -0
- package/dist/templates/data-filler-lite/README.md.template +47 -0
- package/dist/templates/data-filler-lite/eslint.config.mjs +19 -0
- package/dist/templates/data-filler-lite/manifest.json.template +29 -0
- package/dist/templates/data-filler-lite/package.json.template +18 -0
- package/dist/templates/data-filler-lite/src/main.ts.template +222 -0
- package/dist/templates/data-filler-lite/tsconfig.json +14 -0
- package/dist/templates/data-filler-lite/ui.html.template +180 -0
- package/dist/templates/design-lint-panel/README.md.template +33 -0
- package/dist/templates/design-lint-panel/eslint.config.mjs +19 -0
- package/dist/templates/design-lint-panel/manifest.json.template +29 -0
- package/dist/templates/design-lint-panel/package.json.template +18 -0
- package/dist/templates/design-lint-panel/src/main.ts.template +221 -0
- package/dist/templates/design-lint-panel/tsconfig.json +14 -0
- package/dist/templates/design-lint-panel/ui.html.template +172 -0
- package/dist/templates/export-selection/README.md.template +26 -0
- package/dist/templates/export-selection/eslint.config.mjs +19 -0
- package/dist/templates/export-selection/manifest.json.template +31 -0
- package/dist/templates/export-selection/package.json.template +18 -0
- package/dist/templates/export-selection/src/main.ts.template +386 -0
- package/dist/templates/export-selection/tsconfig.json +14 -0
- package/dist/templates/export-selection/ui.html.template +163 -0
- package/dist/templates/review-submitter/README.md.template +24 -0
- package/dist/templates/review-submitter/eslint.config.mjs +19 -0
- package/dist/templates/review-submitter/manifest.json.template +35 -0
- package/dist/templates/review-submitter/package.json.template +18 -0
- package/dist/templates/review-submitter/src/main.ts.template +306 -0
- package/dist/templates/review-submitter/tsconfig.json +14 -0
- package/dist/templates/review-submitter/ui.html.template +114 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +42 -0
- package/dist/validate.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { definePlugin, type PluginContext } from "@lovision/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
type SceneSnapshot = Awaited<ReturnType<PluginContext["document"]["snapshot"]>>;
|
|
4
|
+
type SceneNodeSnapshot = SceneSnapshot["root"];
|
|
5
|
+
type ExportSelectionFormat = "json" | "png" | "svg";
|
|
6
|
+
|
|
7
|
+
type DownloadArtifact =
|
|
8
|
+
| {
|
|
9
|
+
kind: "bytes";
|
|
10
|
+
bytes: number[];
|
|
11
|
+
fileName: string;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
kind: "text";
|
|
16
|
+
fileName: string;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
text: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ExportSelectionMessage =
|
|
22
|
+
| {
|
|
23
|
+
type: "error";
|
|
24
|
+
payload: {
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
type: "exported";
|
|
30
|
+
payload: {
|
|
31
|
+
download: DownloadArtifact;
|
|
32
|
+
preview: string;
|
|
33
|
+
status: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
type: "ready";
|
|
38
|
+
payload: {
|
|
39
|
+
message: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
definePlugin({
|
|
44
|
+
apiVersion: "1.0",
|
|
45
|
+
version: "0.1.0",
|
|
46
|
+
command: async (ctx) => {
|
|
47
|
+
const session = await ctx.ui.show({
|
|
48
|
+
entry: "./ui.html",
|
|
49
|
+
title: "Export Selection",
|
|
50
|
+
width: 680,
|
|
51
|
+
height: 560,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let closed = false;
|
|
55
|
+
const postToUi = async (
|
|
56
|
+
message: ExportSelectionMessage,
|
|
57
|
+
): Promise<void> => {
|
|
58
|
+
if (closed) return;
|
|
59
|
+
await session.postMessage(message);
|
|
60
|
+
};
|
|
61
|
+
const modalClosed = new Promise<void>((resolve) => {
|
|
62
|
+
session.on("close", () => {
|
|
63
|
+
closed = true;
|
|
64
|
+
void ctx.notify.send("Export Selection closed.", { kind: "info" });
|
|
65
|
+
resolve();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
session.on("export-selection", async (payload) => {
|
|
70
|
+
await handleUiAction(postToUi, async () => {
|
|
71
|
+
const format = readExportFormat(payload);
|
|
72
|
+
const artifact =
|
|
73
|
+
format === "png"
|
|
74
|
+
? await exportSelectionWithCurrentOrFirstNode(ctx, {
|
|
75
|
+
format,
|
|
76
|
+
fileName: "selection.png",
|
|
77
|
+
scale: 1,
|
|
78
|
+
background: "current",
|
|
79
|
+
})
|
|
80
|
+
: await exportSelectionWithCurrentOrFirstNode(ctx, {
|
|
81
|
+
format,
|
|
82
|
+
fileName: `selection.${format}`,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (artifact.delivery === "inline") {
|
|
86
|
+
const summary = summarizeInlineArtifact(artifact.format, artifact.text);
|
|
87
|
+
await postToUi({
|
|
88
|
+
type: "exported",
|
|
89
|
+
payload: {
|
|
90
|
+
status: summary.status,
|
|
91
|
+
preview: summary.preview,
|
|
92
|
+
download: {
|
|
93
|
+
kind: "text",
|
|
94
|
+
fileName:
|
|
95
|
+
artifact.fileName ?? defaultFileNameForFormat(format),
|
|
96
|
+
mimeType: artifact.mimeType,
|
|
97
|
+
text: artifact.text,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
await ctx.notify.send(
|
|
102
|
+
`Export Selection exported ${artifact.format} (${artifact.byteLength} bytes).`,
|
|
103
|
+
{ kind: "success" },
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const bytes = await ctx.binary.read(artifact.resource);
|
|
109
|
+
await postToUi({
|
|
110
|
+
type: "exported",
|
|
111
|
+
payload: {
|
|
112
|
+
status: `Exported PNG selection raster (${bytes.byteLength} bytes).`,
|
|
113
|
+
preview: JSON.stringify(
|
|
114
|
+
{
|
|
115
|
+
mode: "selection-raster",
|
|
116
|
+
delivery: artifact.delivery,
|
|
117
|
+
format: artifact.format,
|
|
118
|
+
mimeType: artifact.mimeType,
|
|
119
|
+
byteLength: bytes.byteLength,
|
|
120
|
+
resource: {
|
|
121
|
+
id: artifact.resource.id,
|
|
122
|
+
byteLength: artifact.resource.byteLength,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
null,
|
|
126
|
+
2,
|
|
127
|
+
),
|
|
128
|
+
download: binaryDownload({
|
|
129
|
+
bytes,
|
|
130
|
+
fileName:
|
|
131
|
+
artifact.fileName ??
|
|
132
|
+
artifact.resource.name ??
|
|
133
|
+
defaultFileNameForFormat(format),
|
|
134
|
+
mimeType: artifact.mimeType,
|
|
135
|
+
}),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
await ctx.notify.send(
|
|
139
|
+
`Export Selection exported ${artifact.format} (${bytes.byteLength} bytes).`,
|
|
140
|
+
{ kind: "success" },
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
session.on("capture-screenshot", async () => {
|
|
146
|
+
await handleUiAction(postToUi, async () => {
|
|
147
|
+
const resource = await captureScreenshotWithCurrentOrFirstNode(ctx);
|
|
148
|
+
const bytes = await ctx.binary.read(resource);
|
|
149
|
+
await postToUi({
|
|
150
|
+
type: "exported",
|
|
151
|
+
payload: {
|
|
152
|
+
status: `Captured Screen Preview ${resource.mimeType} (${bytes.byteLength} bytes).`,
|
|
153
|
+
preview: JSON.stringify(
|
|
154
|
+
{
|
|
155
|
+
mode: "screen-preview",
|
|
156
|
+
format: "png",
|
|
157
|
+
mimeType: resource.mimeType,
|
|
158
|
+
byteLength: bytes.byteLength,
|
|
159
|
+
resource: {
|
|
160
|
+
id: resource.id,
|
|
161
|
+
byteLength: resource.byteLength,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
null,
|
|
165
|
+
2,
|
|
166
|
+
),
|
|
167
|
+
download: binaryDownload({
|
|
168
|
+
bytes,
|
|
169
|
+
fileName: resource.name ?? "screenshot.png",
|
|
170
|
+
mimeType: resource.mimeType,
|
|
171
|
+
}),
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
await ctx.notify.send(
|
|
175
|
+
`Export Selection captured screenshot (${bytes.byteLength} bytes).`,
|
|
176
|
+
{ kind: "success" },
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await postToUi({
|
|
182
|
+
type: "ready",
|
|
183
|
+
payload: {
|
|
184
|
+
message:
|
|
185
|
+
"Ready. Export JSON snapshots, SVG paths, PNG selection rasters, or screen previews.",
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await modalClosed;
|
|
190
|
+
return { closed: true, ok: true };
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
async function handleUiAction(
|
|
195
|
+
postToUi: (message: ExportSelectionMessage) => Promise<void>,
|
|
196
|
+
action: () => Promise<unknown>,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
try {
|
|
199
|
+
await action();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (isUnsupportedSelectionRasterError(error)) {
|
|
202
|
+
await postToUi({
|
|
203
|
+
type: "error",
|
|
204
|
+
payload: {
|
|
205
|
+
message:
|
|
206
|
+
"PNG selection export is unsupported until the host provides clean selection-bounds raster capture. Use Capture Screen Preview for a screen-pixel screenshot.",
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
await postToUi({ type: "error", payload: { message } });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function exportSelectionWithCurrentOrFirstNode(
|
|
217
|
+
ctx: PluginContext,
|
|
218
|
+
params: Parameters<PluginContext["export"]["selection"]>[0],
|
|
219
|
+
): ReturnType<PluginContext["export"]["selection"]> {
|
|
220
|
+
try {
|
|
221
|
+
return await ctx.export.selection(params);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (!isEmptySelectionError(error)) {
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return ctx.export.selection({
|
|
228
|
+
...params,
|
|
229
|
+
nodeIds: await resolveFallbackExportNodeIds(ctx),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function captureScreenshotWithCurrentOrFirstNode(ctx: PluginContext) {
|
|
234
|
+
const params = {
|
|
235
|
+
format: "png" as const,
|
|
236
|
+
fileName: "screenshot.png",
|
|
237
|
+
scale: 1,
|
|
238
|
+
background: "current" as const,
|
|
239
|
+
};
|
|
240
|
+
try {
|
|
241
|
+
return await ctx.export.screenshot(params);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (!isEmptySelectionError(error)) {
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return ctx.export.screenshot({
|
|
248
|
+
...params,
|
|
249
|
+
nodeIds: await resolveFallbackExportNodeIds(ctx),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function resolveFallbackExportNodeIds(ctx: PluginContext): Promise<string[]> {
|
|
254
|
+
const snapshot = await ctx.document.snapshot();
|
|
255
|
+
const candidates = collectNodes(snapshot.root).filter(
|
|
256
|
+
(node) => node.id !== snapshot.root.id,
|
|
257
|
+
);
|
|
258
|
+
const first = candidates[0];
|
|
259
|
+
if (!first) {
|
|
260
|
+
throw new Error("Export Selection needs at least one canvas node.");
|
|
261
|
+
}
|
|
262
|
+
return [first.id];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function collectNodes(node: SceneNodeSnapshot): SceneNodeSnapshot[] {
|
|
266
|
+
return [node, ...node.children.flatMap((child) => collectNodes(child))];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function binaryDownload(input: {
|
|
270
|
+
bytes: ArrayBuffer;
|
|
271
|
+
fileName: string;
|
|
272
|
+
mimeType: string;
|
|
273
|
+
}): DownloadArtifact {
|
|
274
|
+
return {
|
|
275
|
+
kind: "bytes",
|
|
276
|
+
bytes: Array.from(new Uint8Array(input.bytes)),
|
|
277
|
+
fileName: input.fileName,
|
|
278
|
+
mimeType: input.mimeType,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function defaultFileNameForFormat(format: ExportSelectionFormat): string {
|
|
283
|
+
return `selection.${format}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function summarizeInlineArtifact(
|
|
287
|
+
format: "json" | "svg",
|
|
288
|
+
text: string,
|
|
289
|
+
): { status: string; preview: string } {
|
|
290
|
+
if (format === "json") {
|
|
291
|
+
return summarizeJsonArtifact(text);
|
|
292
|
+
}
|
|
293
|
+
return summarizeSvgArtifact(text);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function summarizeJsonArtifact(text: string): { status: string; preview: string } {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(text);
|
|
299
|
+
const nodes = Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
300
|
+
const vectorCommandCount = nodes.reduce((sum: number, node: unknown) => {
|
|
301
|
+
if (!isRecord(node) || !isRecord(node.vector)) {
|
|
302
|
+
return sum;
|
|
303
|
+
}
|
|
304
|
+
const commands = node.vector.commands;
|
|
305
|
+
return sum + (Array.isArray(commands) ? commands.length : 0);
|
|
306
|
+
}, 0);
|
|
307
|
+
const summary = {
|
|
308
|
+
mode: "selection-json",
|
|
309
|
+
kind: parsed.kind,
|
|
310
|
+
bounds: parsed.bounds,
|
|
311
|
+
nodeCount: nodes.length,
|
|
312
|
+
vectorCommandCount,
|
|
313
|
+
};
|
|
314
|
+
return {
|
|
315
|
+
status: `Exported JSON snapshot: kind=${String(parsed.kind)} nodes=${nodes.length} vectorCommands=${vectorCommandCount}.`,
|
|
316
|
+
preview: `${JSON.stringify(summary, null, 2)}\n\n${text.slice(0, 1200)}`,
|
|
317
|
+
};
|
|
318
|
+
} catch {
|
|
319
|
+
return {
|
|
320
|
+
status: "Exported JSON snapshot.",
|
|
321
|
+
preview: text.slice(0, 1600),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function summarizeSvgArtifact(text: string): { status: string; preview: string } {
|
|
327
|
+
const pathCount = countMatches(text, /<path(?:\s|>)/g);
|
|
328
|
+
const fallbackCount = countMatches(text, /data-export-fallback=/g);
|
|
329
|
+
const summary = {
|
|
330
|
+
mode: "selection-svg",
|
|
331
|
+
pathCount,
|
|
332
|
+
fallbackCount,
|
|
333
|
+
};
|
|
334
|
+
return {
|
|
335
|
+
status: `Exported SVG paths: paths=${pathCount} fallbacks=${fallbackCount}.`,
|
|
336
|
+
preview: `${JSON.stringify(summary, null, 2)}\n\n${text.slice(0, 1400)}`,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function countMatches(text: string, pattern: RegExp): number {
|
|
341
|
+
return Array.from(text.matchAll(pattern)).length;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isEmptySelectionError(error: unknown): boolean {
|
|
345
|
+
const data = readErrorData(error);
|
|
346
|
+
return (
|
|
347
|
+
readErrorCode(error) === "INVALID_PARAMS" &&
|
|
348
|
+
isRecord(data) &&
|
|
349
|
+
data.reason === "EMPTY_SELECTION"
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isUnsupportedSelectionRasterError(error: unknown): boolean {
|
|
354
|
+
const data = readErrorData(error);
|
|
355
|
+
return (
|
|
356
|
+
readErrorCode(error) === "INVALID_PARAMS" &&
|
|
357
|
+
isRecord(data) &&
|
|
358
|
+
data.reason === "UNSUPPORTED_EXPORT_SEMANTICS"
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function readErrorCode(error: unknown): unknown {
|
|
363
|
+
return isRecord(error) ? error.code : undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function readErrorData(error: unknown): unknown {
|
|
367
|
+
return isRecord(error) ? error.data : undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function readExportFormat(payload: unknown): ExportSelectionFormat {
|
|
371
|
+
if (!isRecord(payload) || typeof payload.format !== "string") {
|
|
372
|
+
throw new Error("Export Selection requires a format.");
|
|
373
|
+
}
|
|
374
|
+
if (
|
|
375
|
+
payload.format === "json" ||
|
|
376
|
+
payload.format === "png" ||
|
|
377
|
+
payload.format === "svg"
|
|
378
|
+
) {
|
|
379
|
+
return payload.format;
|
|
380
|
+
}
|
|
381
|
+
throw new Error(`Unsupported export format: ${payload.format}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
385
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
386
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext", "WebWorker"],
|
|
4
|
+
"module": "Preserve",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"target": "ESNext",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"noEmit": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Export Selection</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light dark;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
color: CanvasText;
|
|
15
|
+
background: Canvas;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
main {
|
|
19
|
+
display: grid;
|
|
20
|
+
gap: 1rem;
|
|
21
|
+
padding: 1rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.actions {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-wrap: wrap;
|
|
27
|
+
gap: 0.5rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.status {
|
|
31
|
+
min-height: 1.5rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pre {
|
|
35
|
+
max-height: 16rem;
|
|
36
|
+
overflow: auto;
|
|
37
|
+
padding: 0.75rem;
|
|
38
|
+
color: CanvasText;
|
|
39
|
+
background: Canvas;
|
|
40
|
+
border: 1px solid ButtonBorder;
|
|
41
|
+
white-space: pre-wrap;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<main>
|
|
47
|
+
<header>
|
|
48
|
+
<h1>Export Selection</h1>
|
|
49
|
+
<p>
|
|
50
|
+
Export the current object selection, or the first canvas node when
|
|
51
|
+
nothing is selected.
|
|
52
|
+
</p>
|
|
53
|
+
</header>
|
|
54
|
+
|
|
55
|
+
<div class="actions">
|
|
56
|
+
<button id="export-json-button" type="button">Export JSON Snapshot</button>
|
|
57
|
+
<button id="export-svg-button" type="button">Export SVG Paths</button>
|
|
58
|
+
<button id="export-png-button" type="button">Export PNG Selection</button>
|
|
59
|
+
<button id="capture-button" type="button">Capture Screen Preview</button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<p id="status" class="status" role="status">Waiting for host...</p>
|
|
63
|
+
<a id="download-link" hidden>Download last export</a>
|
|
64
|
+
<pre id="preview" aria-label="Export preview"></pre>
|
|
65
|
+
</main>
|
|
66
|
+
|
|
67
|
+
<script>
|
|
68
|
+
const uiId = globalThis.__LOVISION_PLUGIN_UI_ID__;
|
|
69
|
+
const status = document.getElementById("status");
|
|
70
|
+
const preview = document.getElementById("preview");
|
|
71
|
+
const downloadLink = document.getElementById("download-link");
|
|
72
|
+
let currentDownload = null;
|
|
73
|
+
|
|
74
|
+
function post(type, payload) {
|
|
75
|
+
parent.postMessage(
|
|
76
|
+
{
|
|
77
|
+
type: "plugin-ui-message",
|
|
78
|
+
direction: "ui-to-main",
|
|
79
|
+
uiId,
|
|
80
|
+
message: { type, payload },
|
|
81
|
+
},
|
|
82
|
+
"*",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setRunning(label) {
|
|
87
|
+
status.textContent = label;
|
|
88
|
+
preview.textContent = "";
|
|
89
|
+
setDownload(null);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function setDownload(download) {
|
|
93
|
+
currentDownload = download;
|
|
94
|
+
downloadLink.hidden = !download;
|
|
95
|
+
downloadLink.href = "#";
|
|
96
|
+
if (!download) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
downloadLink.textContent = `Download ${download.fileName}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
downloadLink.addEventListener("click", (event) => {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
if (!currentDownload) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
parent.postMessage(
|
|
108
|
+
{
|
|
109
|
+
type: "plugin-ui-download",
|
|
110
|
+
uiId,
|
|
111
|
+
...currentDownload,
|
|
112
|
+
},
|
|
113
|
+
"*",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
document.getElementById("export-json-button").addEventListener("click", () => {
|
|
118
|
+
setRunning("Exporting JSON snapshot...");
|
|
119
|
+
post("export-selection", { format: "json" });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
document.getElementById("export-svg-button").addEventListener("click", () => {
|
|
123
|
+
setRunning("Exporting SVG paths...");
|
|
124
|
+
post("export-selection", { format: "svg" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
document.getElementById("export-png-button").addEventListener("click", () => {
|
|
128
|
+
setRunning("Exporting PNG selection...");
|
|
129
|
+
post("export-selection", { format: "png" });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
document.getElementById("capture-button").addEventListener("click", () => {
|
|
133
|
+
setRunning("Capturing screen preview...");
|
|
134
|
+
post("capture-screenshot", {});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
window.addEventListener("message", (event) => {
|
|
138
|
+
const envelope = event.data;
|
|
139
|
+
if (
|
|
140
|
+
!envelope ||
|
|
141
|
+
envelope.type !== "plugin-ui-message" ||
|
|
142
|
+
envelope.direction !== "main-to-ui" ||
|
|
143
|
+
envelope.uiId !== uiId
|
|
144
|
+
) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const message = envelope.message;
|
|
149
|
+
if (message.type === "ready") {
|
|
150
|
+
status.textContent = message.payload.message;
|
|
151
|
+
} else if (message.type === "exported") {
|
|
152
|
+
status.textContent = message.payload.status;
|
|
153
|
+
preview.textContent = message.payload.preview;
|
|
154
|
+
setDownload(message.payload.download);
|
|
155
|
+
} else if (message.type === "error") {
|
|
156
|
+
status.textContent = message.payload.message;
|
|
157
|
+
preview.textContent = "";
|
|
158
|
+
setDownload(null);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
</script>
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Review Submitter
|
|
2
|
+
|
|
3
|
+
This acceptance template captures the current editor context, selection metadata, and a screen preview, then submits a design review payload through `ctx.workflow.submit`. It proves the plugin can read host-owned context with `ctx.context.current`, preserve a binary screenshot artifact ref, and hand the result to a host workflow target without direct network or token access.
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Copy the printed `manifestUrl=...`, open the editor shell, then use Dev Panel -> Development -> Add by URL.
|
|
13
|
+
|
|
14
|
+
## Use
|
|
15
|
+
|
|
16
|
+
Run **Open Review Submitter**, then click **Capture review snapshot**. The UI shows the user/workspace context, selection count, node count, and screenshot resource metadata. Click **Submit review** to call `ctx.workflow.submit` with target `review.submit`.
|
|
17
|
+
|
|
18
|
+
## Formal install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Upload the generated `dist/*.bundle.json` through Dev Panel -> Formal install, confirm the install dialog, then run the command from MainMenu.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pluginSdkConfig from "@lovision/plugin-sdk/eslint-config";
|
|
2
|
+
import tsParser from "@typescript-eslint/parser";
|
|
3
|
+
|
|
4
|
+
const pluginFiles = ["src/**/*.ts"];
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
{
|
|
8
|
+
files: pluginFiles,
|
|
9
|
+
languageOptions: {
|
|
10
|
+
parser: tsParser,
|
|
11
|
+
ecmaVersion: "latest",
|
|
12
|
+
sourceType: "module",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
...pluginSdkConfig.map((entry) => ({
|
|
16
|
+
...entry,
|
|
17
|
+
files: pluginFiles,
|
|
18
|
+
})),
|
|
19
|
+
];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "__PLUGIN_ID__",
|
|
3
|
+
"name": {
|
|
4
|
+
"en": "Review Submitter",
|
|
5
|
+
"zh-CN": "Review Submitter"
|
|
6
|
+
},
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"apiVersion": "1.0",
|
|
9
|
+
"editorType": ["design"],
|
|
10
|
+
"main": "./dist/main.js",
|
|
11
|
+
"ui": "./ui.html",
|
|
12
|
+
"documentAccess": "current-page",
|
|
13
|
+
"permissions": [
|
|
14
|
+
"context:read.user",
|
|
15
|
+
"context:read.workspace",
|
|
16
|
+
"document:read",
|
|
17
|
+
"export:screenshot",
|
|
18
|
+
"notify",
|
|
19
|
+
"selection:read",
|
|
20
|
+
"ui:modal",
|
|
21
|
+
"workflow:submit"
|
|
22
|
+
],
|
|
23
|
+
"workflowAccess": {
|
|
24
|
+
"allowedTargets": ["review.submit"]
|
|
25
|
+
},
|
|
26
|
+
"commands": [
|
|
27
|
+
{
|
|
28
|
+
"id": "run",
|
|
29
|
+
"name": {
|
|
30
|
+
"en": "__COMMAND_NAME_EN__",
|
|
31
|
+
"zh-CN": "__COMMAND_NAME_ZH__"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PACKAGE_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "plugin-dev dev",
|
|
7
|
+
"build": "plugin-dev build",
|
|
8
|
+
"validate": "plugin-dev validate",
|
|
9
|
+
"lint": "eslint src --max-warnings=0"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@lovision/plugin-dev": "__PLUGIN_DEV_SPEC__",
|
|
13
|
+
"@lovision/plugin-sdk": "__PLUGIN_SDK_SPEC__"__LOCAL_SUPPORT_DEPS__,
|
|
14
|
+
"@typescript-eslint/parser": "^8.30.0",
|
|
15
|
+
"eslint": "^9.25.1",
|
|
16
|
+
"typescript": "^5.8.3"
|
|
17
|
+
}__LOCAL_OVERRIDES__
|
|
18
|
+
}
|