@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.
Files changed (96) hide show
  1. package/README.md +49 -0
  2. package/dist/build.d.ts +16 -0
  3. package/dist/build.d.ts.map +1 -0
  4. package/dist/build.js +108 -0
  5. package/dist/build.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +123 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/create-plugin.d.ts +19 -0
  11. package/dist/create-plugin.d.ts.map +1 -0
  12. package/dist/create-plugin.js +186 -0
  13. package/dist/create-plugin.js.map +1 -0
  14. package/dist/dev.d.ts +13 -0
  15. package/dist/dev.d.ts.map +1 -0
  16. package/dist/dev.js +206 -0
  17. package/dist/dev.js.map +1 -0
  18. package/dist/index.d.ts +13 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +7 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/publish.d.ts +15 -0
  23. package/dist/publish.d.ts.map +1 -0
  24. package/dist/publish.js +55 -0
  25. package/dist/publish.js.map +1 -0
  26. package/dist/shared.d.ts +93 -0
  27. package/dist/shared.d.ts.map +1 -0
  28. package/dist/shared.js +436 -0
  29. package/dist/shared.js.map +1 -0
  30. package/dist/templates/ai-layout-assistant/README.md.template +24 -0
  31. package/dist/templates/ai-layout-assistant/eslint.config.mjs +19 -0
  32. package/dist/templates/ai-layout-assistant/manifest.json.template +38 -0
  33. package/dist/templates/ai-layout-assistant/package.json.template +18 -0
  34. package/dist/templates/ai-layout-assistant/src/main.ts.template +345 -0
  35. package/dist/templates/ai-layout-assistant/tsconfig.json +14 -0
  36. package/dist/templates/ai-layout-assistant/ui.html.template +114 -0
  37. package/dist/templates/asset-browser/README.md.template +24 -0
  38. package/dist/templates/asset-browser/eslint.config.mjs +19 -0
  39. package/dist/templates/asset-browser/manifest.json.template +29 -0
  40. package/dist/templates/asset-browser/package.json.template +18 -0
  41. package/dist/templates/asset-browser/src/main.ts.template +177 -0
  42. package/dist/templates/asset-browser/tsconfig.json +14 -0
  43. package/dist/templates/asset-browser/ui.html.template +137 -0
  44. package/dist/templates/base/README.md.template +34 -0
  45. package/dist/templates/base/eslint.config.mjs +19 -0
  46. package/dist/templates/base/manifest.json.template +22 -0
  47. package/dist/templates/base/package.json.template +18 -0
  48. package/dist/templates/base/src/main.ts.template +20 -0
  49. package/dist/templates/base/tsconfig.json +14 -0
  50. package/dist/templates/batch-layout-organizer/README.md.template +24 -0
  51. package/dist/templates/batch-layout-organizer/eslint.config.mjs +19 -0
  52. package/dist/templates/batch-layout-organizer/manifest.json.template +31 -0
  53. package/dist/templates/batch-layout-organizer/package.json.template +18 -0
  54. package/dist/templates/batch-layout-organizer/src/main.ts.template +324 -0
  55. package/dist/templates/batch-layout-organizer/tsconfig.json +14 -0
  56. package/dist/templates/batch-layout-organizer/ui.html.template +116 -0
  57. package/dist/templates/data-filler-full/README.md.template +32 -0
  58. package/dist/templates/data-filler-full/eslint.config.mjs +19 -0
  59. package/dist/templates/data-filler-full/manifest.json.template +31 -0
  60. package/dist/templates/data-filler-full/package.json.template +18 -0
  61. package/dist/templates/data-filler-full/src/main.ts.template +412 -0
  62. package/dist/templates/data-filler-full/tsconfig.json +14 -0
  63. package/dist/templates/data-filler-full/ui.html.template +221 -0
  64. package/dist/templates/data-filler-lite/README.md.template +47 -0
  65. package/dist/templates/data-filler-lite/eslint.config.mjs +19 -0
  66. package/dist/templates/data-filler-lite/manifest.json.template +29 -0
  67. package/dist/templates/data-filler-lite/package.json.template +18 -0
  68. package/dist/templates/data-filler-lite/src/main.ts.template +222 -0
  69. package/dist/templates/data-filler-lite/tsconfig.json +14 -0
  70. package/dist/templates/data-filler-lite/ui.html.template +180 -0
  71. package/dist/templates/design-lint-panel/README.md.template +33 -0
  72. package/dist/templates/design-lint-panel/eslint.config.mjs +19 -0
  73. package/dist/templates/design-lint-panel/manifest.json.template +29 -0
  74. package/dist/templates/design-lint-panel/package.json.template +18 -0
  75. package/dist/templates/design-lint-panel/src/main.ts.template +221 -0
  76. package/dist/templates/design-lint-panel/tsconfig.json +14 -0
  77. package/dist/templates/design-lint-panel/ui.html.template +172 -0
  78. package/dist/templates/export-selection/README.md.template +26 -0
  79. package/dist/templates/export-selection/eslint.config.mjs +19 -0
  80. package/dist/templates/export-selection/manifest.json.template +31 -0
  81. package/dist/templates/export-selection/package.json.template +18 -0
  82. package/dist/templates/export-selection/src/main.ts.template +386 -0
  83. package/dist/templates/export-selection/tsconfig.json +14 -0
  84. package/dist/templates/export-selection/ui.html.template +163 -0
  85. package/dist/templates/review-submitter/README.md.template +24 -0
  86. package/dist/templates/review-submitter/eslint.config.mjs +19 -0
  87. package/dist/templates/review-submitter/manifest.json.template +35 -0
  88. package/dist/templates/review-submitter/package.json.template +18 -0
  89. package/dist/templates/review-submitter/src/main.ts.template +306 -0
  90. package/dist/templates/review-submitter/tsconfig.json +14 -0
  91. package/dist/templates/review-submitter/ui.html.template +114 -0
  92. package/dist/validate.d.ts +8 -0
  93. package/dist/validate.d.ts.map +1 -0
  94. package/dist/validate.js +42 -0
  95. package/dist/validate.js.map +1 -0
  96. 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
+ ];