@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,412 @@
|
|
|
1
|
+
import { definePlugin } from "@lovision/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
type DataRow = Record<string, string>;
|
|
4
|
+
|
|
5
|
+
type Vec2 = {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type Size = {
|
|
11
|
+
height: number;
|
|
12
|
+
width: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type SceneNodeSnapshot = {
|
|
16
|
+
children: SceneNodeSnapshot[];
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
opacity: number;
|
|
20
|
+
parentId: string | null;
|
|
21
|
+
position: Vec2;
|
|
22
|
+
size: Size;
|
|
23
|
+
text?: {
|
|
24
|
+
content: string;
|
|
25
|
+
};
|
|
26
|
+
type: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type SceneSnapshot = {
|
|
30
|
+
root: SceneNodeSnapshot;
|
|
31
|
+
version: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type NodeCreateSpec = {
|
|
35
|
+
extraFields?: Record<string, unknown>;
|
|
36
|
+
name?: string;
|
|
37
|
+
parentId?: string | null;
|
|
38
|
+
position?: Vec2;
|
|
39
|
+
size?: Size;
|
|
40
|
+
type: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type MutationResult = {
|
|
44
|
+
newVersion: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type NodesApi = {
|
|
48
|
+
create(
|
|
49
|
+
specs: NodeCreateSpec[],
|
|
50
|
+
opts?: { expectedVersion?: number },
|
|
51
|
+
): Promise<MutationResult>;
|
|
52
|
+
setText(
|
|
53
|
+
updates: Array<{ id: string; text: string }>,
|
|
54
|
+
opts?: { expectedVersion?: number },
|
|
55
|
+
): Promise<MutationResult>;
|
|
56
|
+
update(
|
|
57
|
+
updates: Array<{
|
|
58
|
+
changes: {
|
|
59
|
+
name?: string;
|
|
60
|
+
opacity?: number;
|
|
61
|
+
};
|
|
62
|
+
id: string;
|
|
63
|
+
}>,
|
|
64
|
+
opts?: { expectedVersion?: number },
|
|
65
|
+
): Promise<MutationResult>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type DataFillerContext = {
|
|
69
|
+
document: {
|
|
70
|
+
snapshot(): Promise<SceneSnapshot>;
|
|
71
|
+
};
|
|
72
|
+
nodes: NodesApi;
|
|
73
|
+
notify: {
|
|
74
|
+
send(
|
|
75
|
+
message: string,
|
|
76
|
+
options?: { kind?: "error" | "info" | "success" | "warning" },
|
|
77
|
+
): Promise<void>;
|
|
78
|
+
};
|
|
79
|
+
selection: {
|
|
80
|
+
get(): Promise<string[]>;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type ApplyPayload = {
|
|
85
|
+
nameField: string;
|
|
86
|
+
opacityField: string;
|
|
87
|
+
text: string;
|
|
88
|
+
textField: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type UserDecision =
|
|
92
|
+
| {
|
|
93
|
+
kind: "apply";
|
|
94
|
+
payload: unknown;
|
|
95
|
+
}
|
|
96
|
+
| {
|
|
97
|
+
kind: "close";
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
definePlugin({
|
|
101
|
+
apiVersion: "1.0",
|
|
102
|
+
version: "0.1.0",
|
|
103
|
+
command: async (ctx) => {
|
|
104
|
+
const session = await ctx.ui.show({
|
|
105
|
+
entry: "./ui.html",
|
|
106
|
+
title: "Data Filler",
|
|
107
|
+
width: 600,
|
|
108
|
+
height: 600,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
let closed = false;
|
|
112
|
+
const userDecision = new Promise<UserDecision>((resolve) => {
|
|
113
|
+
session.on("close", () => {
|
|
114
|
+
closed = true;
|
|
115
|
+
resolve({ kind: "close" });
|
|
116
|
+
});
|
|
117
|
+
session.on("apply-mapping", (payload) => {
|
|
118
|
+
resolve({ kind: "apply", payload });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await session.postMessage({
|
|
123
|
+
type: "ready",
|
|
124
|
+
payload: {
|
|
125
|
+
message:
|
|
126
|
+
"Paste CSV or JSON, choose mapping fields, then apply to text nodes.",
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const decision = await userDecision;
|
|
131
|
+
if (decision.kind === "close") {
|
|
132
|
+
await ctx.notify.send("Data Filler closed without applying data.", {
|
|
133
|
+
kind: "info",
|
|
134
|
+
});
|
|
135
|
+
return { applied: 0, closed: true, ok: true };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const payload = readApplyPayload(decision.payload);
|
|
140
|
+
const rows = parseRows(payload.text);
|
|
141
|
+
const targets = await resolveTargetTextNodes(ctx, rows.length);
|
|
142
|
+
const textUpdates = targets.nodes.map((node, index) => ({
|
|
143
|
+
id: node.id,
|
|
144
|
+
text: valueForField(
|
|
145
|
+
rows[index % rows.length],
|
|
146
|
+
payload.textField,
|
|
147
|
+
`Data Row ${index + 1}`,
|
|
148
|
+
),
|
|
149
|
+
}));
|
|
150
|
+
const textResult = await ctx.nodes.setText(textUpdates, {
|
|
151
|
+
expectedVersion: targets.version,
|
|
152
|
+
});
|
|
153
|
+
const styleUpdates = targets.nodes.map((node, index) => {
|
|
154
|
+
const row = rows[index % rows.length];
|
|
155
|
+
return {
|
|
156
|
+
id: node.id,
|
|
157
|
+
changes: {
|
|
158
|
+
name: valueForField(row, payload.nameField, node.name),
|
|
159
|
+
opacity: opacityForField(row, payload.opacityField, node.opacity),
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
const styleResult = await ctx.nodes.update(styleUpdates, {
|
|
164
|
+
expectedVersion: textResult.newVersion,
|
|
165
|
+
});
|
|
166
|
+
if (!closed) {
|
|
167
|
+
await session.postMessage({
|
|
168
|
+
type: "applied",
|
|
169
|
+
payload: {
|
|
170
|
+
applied: textUpdates.length,
|
|
171
|
+
newVersion: styleResult.newVersion,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
await ctx.notify.send(
|
|
176
|
+
`Data Filler applied ${textUpdates.length} text node(s).`,
|
|
177
|
+
{ kind: "success" },
|
|
178
|
+
);
|
|
179
|
+
return {
|
|
180
|
+
applied: textUpdates.length,
|
|
181
|
+
newVersion: styleResult.newVersion,
|
|
182
|
+
ok: true,
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
if (!closed) {
|
|
187
|
+
await session.postMessage({
|
|
188
|
+
type: "error",
|
|
189
|
+
payload: { message },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
await ctx.notify.send(`Data Filler failed: ${message}`, {
|
|
193
|
+
kind: "error",
|
|
194
|
+
});
|
|
195
|
+
return { error: message, ok: false };
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
function readApplyPayload(payload: unknown): ApplyPayload {
|
|
201
|
+
if (!isRecord(payload) || typeof payload.text !== "string") {
|
|
202
|
+
throw new Error("Apply payload must include a text field.");
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
text: payload.text,
|
|
206
|
+
textField: readFieldName(payload.textField, "title"),
|
|
207
|
+
nameField: readFieldName(payload.nameField, "name"),
|
|
208
|
+
opacityField: readFieldName(payload.opacityField, "opacity"),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function resolveTargetTextNodes(
|
|
213
|
+
ctx: DataFillerContext,
|
|
214
|
+
rowCount: number,
|
|
215
|
+
): Promise<{ nodes: SceneNodeSnapshot[]; version: number }> {
|
|
216
|
+
const snapshot = await ctx.document.snapshot();
|
|
217
|
+
const selection = await ctx.selection.get();
|
|
218
|
+
const allNodes = collectNodes(snapshot.root).filter(
|
|
219
|
+
(node) => node.id !== snapshot.root.id,
|
|
220
|
+
);
|
|
221
|
+
const selectedTextNodes = selection
|
|
222
|
+
.map((nodeId) => allNodes.find((node) => node.id === nodeId))
|
|
223
|
+
.filter(isTextNode);
|
|
224
|
+
const existingTextNodes =
|
|
225
|
+
selectedTextNodes.length > 0 ? selectedTextNodes : allNodes.filter(isTextNode);
|
|
226
|
+
if (existingTextNodes.length > 0) {
|
|
227
|
+
return {
|
|
228
|
+
nodes: existingTextNodes.slice(0, Math.max(1, rowCount)),
|
|
229
|
+
version: snapshot.version,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await ctx.nodes.create([createFallbackTextNode()], {
|
|
234
|
+
expectedVersion: snapshot.version,
|
|
235
|
+
});
|
|
236
|
+
const afterCreate = await ctx.document.snapshot();
|
|
237
|
+
const fallback = collectNodes(afterCreate.root).find(
|
|
238
|
+
(node) => node.type === "text" && node.name === "Data Filler Target",
|
|
239
|
+
);
|
|
240
|
+
if (!fallback) {
|
|
241
|
+
throw new Error("Could not find the fallback text node after creation.");
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
nodes: [fallback],
|
|
245
|
+
version: afterCreate.version,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function createFallbackTextNode(): NodeCreateSpec {
|
|
250
|
+
return {
|
|
251
|
+
type: "text",
|
|
252
|
+
name: "Data Filler Target",
|
|
253
|
+
parentId: null,
|
|
254
|
+
position: { x: 120, y: 360 },
|
|
255
|
+
size: { width: 280, height: 72 },
|
|
256
|
+
extraFields: {
|
|
257
|
+
data: {
|
|
258
|
+
config: {
|
|
259
|
+
text: "Data Filler Target",
|
|
260
|
+
layout: {
|
|
261
|
+
mode: "fixed",
|
|
262
|
+
textAlign: "left",
|
|
263
|
+
verticalAlign: "top",
|
|
264
|
+
paragraphSpacing: 0,
|
|
265
|
+
paragraphIndent: 0,
|
|
266
|
+
},
|
|
267
|
+
baseStyle: {
|
|
268
|
+
fontSize: 16,
|
|
269
|
+
fontWeight: 400,
|
|
270
|
+
fontFamily: "Inter",
|
|
271
|
+
italic: false,
|
|
272
|
+
underline: false,
|
|
273
|
+
strikethrough: false,
|
|
274
|
+
lineHeight: { type: "auto" },
|
|
275
|
+
letterSpacing: { type: "px", value: 0 },
|
|
276
|
+
},
|
|
277
|
+
styleOverrides: [],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function parseRows(input: string): DataRow[] {
|
|
285
|
+
const text = input.trim();
|
|
286
|
+
if (text.length === 0) {
|
|
287
|
+
throw new Error("Paste CSV or JSON before applying.");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (text.startsWith("{") || text.startsWith("[")) {
|
|
291
|
+
return parseJsonRows(text);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return parseCsvRows(text);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseJsonRows(text: string): DataRow[] {
|
|
298
|
+
const parsed = JSON.parse(text) as unknown;
|
|
299
|
+
const values = Array.isArray(parsed) ? parsed : [parsed];
|
|
300
|
+
const rows = values.map((value) => coerceRow(value));
|
|
301
|
+
if (rows.length === 0) {
|
|
302
|
+
throw new Error("JSON input must contain at least one row.");
|
|
303
|
+
}
|
|
304
|
+
return rows;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function parseCsvRows(text: string): DataRow[] {
|
|
308
|
+
const lines = text
|
|
309
|
+
.split(/\r?\n/)
|
|
310
|
+
.map((line) => line.trim())
|
|
311
|
+
.filter((line) => line.length > 0);
|
|
312
|
+
if (lines.length < 2) {
|
|
313
|
+
throw new Error("CSV input needs a header row and at least one data row.");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const headers = parseCsvLine(lines[0]).map((header) => header.trim());
|
|
317
|
+
if (headers.length === 0 || headers.some((header) => header.length === 0)) {
|
|
318
|
+
throw new Error("CSV headers must not be empty.");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return lines.slice(1).map((line) => {
|
|
322
|
+
const values = parseCsvLine(line);
|
|
323
|
+
const row: DataRow = {};
|
|
324
|
+
headers.forEach((header, index) => {
|
|
325
|
+
row[header] = values[index] ?? "";
|
|
326
|
+
});
|
|
327
|
+
return row;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseCsvLine(line: string): string[] {
|
|
332
|
+
const values: string[] = [];
|
|
333
|
+
let current = "";
|
|
334
|
+
let quoted = false;
|
|
335
|
+
|
|
336
|
+
for (const char of line) {
|
|
337
|
+
if (char === '"') {
|
|
338
|
+
quoted = !quoted;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (char === "," && !quoted) {
|
|
342
|
+
values.push(current.trim());
|
|
343
|
+
current = "";
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
current += char;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
values.push(current.trim());
|
|
350
|
+
return values;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function coerceRow(value: unknown): DataRow {
|
|
354
|
+
if (!isRecord(value)) {
|
|
355
|
+
throw new Error("Each JSON row must be an object.");
|
|
356
|
+
}
|
|
357
|
+
const row: DataRow = {};
|
|
358
|
+
for (const [key, cell] of Object.entries(value)) {
|
|
359
|
+
row[key] = cell == null ? "" : String(cell);
|
|
360
|
+
}
|
|
361
|
+
return row;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function valueForField(
|
|
365
|
+
row: DataRow | undefined,
|
|
366
|
+
fieldName: string,
|
|
367
|
+
fallback: string,
|
|
368
|
+
): string {
|
|
369
|
+
const raw = row?.[fieldName];
|
|
370
|
+
const value = typeof raw === "string" ? raw.trim() : "";
|
|
371
|
+
return value.length > 0 ? value : fallback;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function opacityForField(
|
|
375
|
+
row: DataRow | undefined,
|
|
376
|
+
fieldName: string,
|
|
377
|
+
fallback: number,
|
|
378
|
+
): number {
|
|
379
|
+
const raw = row?.[fieldName];
|
|
380
|
+
const parsed = raw === undefined ? Number.NaN : Number.parseFloat(raw);
|
|
381
|
+
if (!Number.isFinite(parsed)) {
|
|
382
|
+
return fallback;
|
|
383
|
+
}
|
|
384
|
+
return Math.min(1, Math.max(0, parsed));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function readFieldName(value: unknown, fallback: string): string {
|
|
388
|
+
return typeof value === "string" && value.trim().length > 0
|
|
389
|
+
? value.trim()
|
|
390
|
+
: fallback;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function collectNodes(
|
|
394
|
+
node: SceneNodeSnapshot,
|
|
395
|
+
out: SceneNodeSnapshot[] = [],
|
|
396
|
+
): SceneNodeSnapshot[] {
|
|
397
|
+
out.push(node);
|
|
398
|
+
for (const child of node.children) {
|
|
399
|
+
collectNodes(child, out);
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isTextNode(
|
|
405
|
+
node: SceneNodeSnapshot | undefined,
|
|
406
|
+
): node is SceneNodeSnapshot {
|
|
407
|
+
return node !== undefined && node.type === "text";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
411
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
412
|
+
}
|
|
@@ -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,221 @@
|
|
|
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>Data Filler</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
|
+
textarea,
|
|
25
|
+
input {
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
width: 100%;
|
|
28
|
+
color: FieldText;
|
|
29
|
+
background: Field;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
textarea {
|
|
33
|
+
min-height: 11rem;
|
|
34
|
+
resize: vertical;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.mapping {
|
|
38
|
+
display: grid;
|
|
39
|
+
gap: 0.75rem;
|
|
40
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.actions {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
gap: 0.5rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.status {
|
|
50
|
+
min-height: 1.5rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pre {
|
|
54
|
+
max-height: 8rem;
|
|
55
|
+
overflow: auto;
|
|
56
|
+
padding: 0.75rem;
|
|
57
|
+
color: CanvasText;
|
|
58
|
+
background: Canvas;
|
|
59
|
+
border: 1px solid ButtonBorder;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@media (max-width: 520px) {
|
|
63
|
+
.mapping {
|
|
64
|
+
grid-template-columns: 1fr;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
68
|
+
</head>
|
|
69
|
+
<body>
|
|
70
|
+
<main>
|
|
71
|
+
<header>
|
|
72
|
+
<h1>Data Filler</h1>
|
|
73
|
+
<p>
|
|
74
|
+
Paste CSV or JSON, map fields, then apply through the plugin main
|
|
75
|
+
worker to text content and node properties.
|
|
76
|
+
</p>
|
|
77
|
+
</header>
|
|
78
|
+
|
|
79
|
+
<label>
|
|
80
|
+
Data
|
|
81
|
+
<textarea id="data-input" spellcheck="false">title,name,opacity
|
|
82
|
+
Hero Headline,Hero Text Node,0.72</textarea>
|
|
83
|
+
</label>
|
|
84
|
+
|
|
85
|
+
<section class="mapping" aria-label="Field mapping">
|
|
86
|
+
<label>
|
|
87
|
+
Text field
|
|
88
|
+
<input id="text-field" value="title" />
|
|
89
|
+
</label>
|
|
90
|
+
<label>
|
|
91
|
+
Name field
|
|
92
|
+
<input id="name-field" value="name" />
|
|
93
|
+
</label>
|
|
94
|
+
<label>
|
|
95
|
+
Opacity field
|
|
96
|
+
<input id="opacity-field" value="opacity" />
|
|
97
|
+
</label>
|
|
98
|
+
</section>
|
|
99
|
+
|
|
100
|
+
<div class="actions">
|
|
101
|
+
<button id="preview-button" type="button">Preview mapping</button>
|
|
102
|
+
<button id="apply-button" type="button">Apply mapping</button>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<p id="status" class="status" role="status">Waiting for host...</p>
|
|
106
|
+
<pre id="preview" aria-label="Preview"></pre>
|
|
107
|
+
</main>
|
|
108
|
+
|
|
109
|
+
<script>
|
|
110
|
+
const uiId = globalThis.__LOVISION_PLUGIN_UI_ID__;
|
|
111
|
+
const dataInput = document.getElementById("data-input");
|
|
112
|
+
const textField = document.getElementById("text-field");
|
|
113
|
+
const nameField = document.getElementById("name-field");
|
|
114
|
+
const opacityField = document.getElementById("opacity-field");
|
|
115
|
+
const status = document.getElementById("status");
|
|
116
|
+
const preview = document.getElementById("preview");
|
|
117
|
+
|
|
118
|
+
function post(type, payload) {
|
|
119
|
+
parent.postMessage(
|
|
120
|
+
{
|
|
121
|
+
type: "plugin-ui-message",
|
|
122
|
+
direction: "ui-to-main",
|
|
123
|
+
uiId,
|
|
124
|
+
message: { type, payload },
|
|
125
|
+
},
|
|
126
|
+
"*",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseRows(text) {
|
|
131
|
+
const trimmed = text.trim();
|
|
132
|
+
if (!trimmed) {
|
|
133
|
+
throw new Error("Paste CSV or JSON before applying.");
|
|
134
|
+
}
|
|
135
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
136
|
+
const parsed = JSON.parse(trimmed);
|
|
137
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
138
|
+
}
|
|
139
|
+
const lines = trimmed
|
|
140
|
+
.split(/\r?\n/)
|
|
141
|
+
.map((line) => line.trim())
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
const headers = splitCsvLine(lines[0] || "");
|
|
144
|
+
return lines.slice(1).map((line) => {
|
|
145
|
+
const values = splitCsvLine(line);
|
|
146
|
+
return Object.fromEntries(
|
|
147
|
+
headers.map((header, index) => [header, values[index] || ""]),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function splitCsvLine(line) {
|
|
153
|
+
const values = [];
|
|
154
|
+
let current = "";
|
|
155
|
+
let quoted = false;
|
|
156
|
+
for (const char of line) {
|
|
157
|
+
if (char === '"') {
|
|
158
|
+
quoted = !quoted;
|
|
159
|
+
} else if (char === "," && !quoted) {
|
|
160
|
+
values.push(current.trim());
|
|
161
|
+
current = "";
|
|
162
|
+
} else {
|
|
163
|
+
current += char;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
values.push(current.trim());
|
|
167
|
+
return values;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function previewRows() {
|
|
171
|
+
const rows = parseRows(dataInput.value);
|
|
172
|
+
const mapped = rows.map((row) => ({
|
|
173
|
+
text: row[textField.value] || "",
|
|
174
|
+
name: row[nameField.value] || "",
|
|
175
|
+
opacity: row[opacityField.value] || "",
|
|
176
|
+
}));
|
|
177
|
+
preview.textContent = JSON.stringify(mapped, null, 2);
|
|
178
|
+
status.textContent = `Previewed ${mapped.length} row(s).`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
document.getElementById("preview-button").addEventListener("click", () => {
|
|
182
|
+
try {
|
|
183
|
+
previewRows();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
status.textContent = error instanceof Error ? error.message : String(error);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
document.getElementById("apply-button").addEventListener("click", () => {
|
|
190
|
+
status.textContent = "Applying...";
|
|
191
|
+
post("apply-mapping", {
|
|
192
|
+
text: dataInput.value,
|
|
193
|
+
textField: textField.value,
|
|
194
|
+
nameField: nameField.value,
|
|
195
|
+
opacityField: opacityField.value,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
window.addEventListener("message", (event) => {
|
|
200
|
+
const envelope = event.data;
|
|
201
|
+
if (
|
|
202
|
+
!envelope ||
|
|
203
|
+
envelope.type !== "plugin-ui-message" ||
|
|
204
|
+
envelope.direction !== "main-to-ui" ||
|
|
205
|
+
envelope.uiId !== uiId
|
|
206
|
+
) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const message = envelope.message;
|
|
211
|
+
if (message.type === "ready") {
|
|
212
|
+
status.textContent = message.payload.message;
|
|
213
|
+
} else if (message.type === "applied") {
|
|
214
|
+
status.textContent = `Applied ${message.payload.applied} text node(s).`;
|
|
215
|
+
} else if (message.type === "error") {
|
|
216
|
+
status.textContent = message.payload.message;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
</script>
|
|
220
|
+
</body>
|
|
221
|
+
</html>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Data Filler Lite
|
|
2
|
+
|
|
3
|
+
Data Filler Lite is a modal-first Instinct UI plugin example generated by `plugin-dev create --template data-filler-lite`.
|
|
4
|
+
|
|
5
|
+
It uses a single `ui.html` resource with an inline browser bridge. The iframe UI never calls the Host Facade directly; it only posts typed messages to the main worker, and the main worker decides when to call `selection`, `document`, `nodes`, and `notify`.
|
|
6
|
+
|
|
7
|
+
## Scripts
|
|
8
|
+
|
|
9
|
+
- `dev` — start the local HTTPS sideload server and print `manifestUrl`
|
|
10
|
+
- `build` — emit a formal-install bundle under `dist/`
|
|
11
|
+
- `validate` — run the same manifest + bundle validation path used by the host
|
|
12
|
+
- `lint` — run the default plugin ESLint setup
|
|
13
|
+
|
|
14
|
+
## First Run
|
|
15
|
+
|
|
16
|
+
1. Install dependencies with your preferred package manager.
|
|
17
|
+
2. Run `dev`.
|
|
18
|
+
3. Open the editor shell Plugin Development panel.
|
|
19
|
+
4. Add by URL with the printed `manifestUrl`.
|
|
20
|
+
5. Run `Open Data Filler Lite`.
|
|
21
|
+
6. Prefer selecting canvas nodes before Apply. If nothing is selected, the Lite demo falls back to the first top-level node so the example remains easy to verify.
|
|
22
|
+
7. Paste CSV or JSON and click Apply.
|
|
23
|
+
|
|
24
|
+
## Input Examples
|
|
25
|
+
|
|
26
|
+
CSV:
|
|
27
|
+
|
|
28
|
+
```csv
|
|
29
|
+
name,role
|
|
30
|
+
Hero Card,Landing
|
|
31
|
+
CTA Button,Marketing
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
JSON:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
[
|
|
38
|
+
{ "name": "Hero Card", "role": "Landing" },
|
|
39
|
+
{ "name": "CTA Button", "role": "Marketing" }
|
|
40
|
+
]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The Lite version maps each row to a node name. A fuller Data Filler can move this mapping to text node contents once a text Host Facade is available.
|
|
44
|
+
|
|
45
|
+
## Formal Install
|
|
46
|
+
|
|
47
|
+
Run `build`, then upload the generated `dist/*.bundle.json` as a Formal install. The installed plugin should appear in Installed and MainMenu, and the same modal UI should open 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
|
+
];
|