ghostfill 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1873 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ fill: () => fill,
24
+ init: () => init
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/types.ts
29
+ var PROVIDERS = {
30
+ openai: { label: "OpenAI", model: "gpt-4o-mini", baseURL: "https://api.openai.com/v1", helpText: "Uses gpt-4o-mini \u2014 fast & cheap" },
31
+ xai: { label: "xAI", model: "grok-4-fast", baseURL: "https://api.x.ai/v1", helpText: "Uses Grok 4 Fast" },
32
+ moonshot: { label: "Moonshot", model: "kimi-k2", baseURL: "https://api.moonshot.ai/v1", helpText: "Uses Kimi K2 \u2014 fast & cheap" }
33
+ };
34
+
35
+ // src/detector.ts
36
+ var INPUT_SELECTORS = [
37
+ "input:not([type=hidden]):not([type=submit]):not([type=button]):not([type=reset]):not([type=image])",
38
+ "textarea",
39
+ "select",
40
+ // Custom dropdown triggers (Headless UI Listbox, Radix, etc.)
41
+ "button[role=combobox]",
42
+ "[role=combobox]:not(input)",
43
+ "button[aria-haspopup=listbox]"
44
+ ].join(", ");
45
+ function findLabel(el) {
46
+ if (el.id) {
47
+ const label = document.querySelector(
48
+ `label[for="${CSS.escape(el.id)}"]`
49
+ );
50
+ if (label?.textContent?.trim()) return label.textContent.trim();
51
+ }
52
+ const parentLabel = el.closest("label");
53
+ if (parentLabel) {
54
+ const clone = parentLabel.cloneNode(true);
55
+ clone.querySelectorAll("input, textarea, select").forEach((c) => c.remove());
56
+ const text = clone.textContent?.trim();
57
+ if (text) return text;
58
+ }
59
+ const ariaLabel = el.getAttribute("aria-label");
60
+ if (ariaLabel?.trim()) return ariaLabel.trim();
61
+ const labelledBy = el.getAttribute("aria-labelledby");
62
+ if (labelledBy) {
63
+ const parts = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean);
64
+ if (parts.length) return parts.join(" ");
65
+ }
66
+ if ("placeholder" in el) {
67
+ const ph = el.placeholder;
68
+ if (ph?.trim()) return ph.trim();
69
+ }
70
+ const title = el.getAttribute("title");
71
+ if (title?.trim()) return title.trim();
72
+ const prev = el.previousElementSibling;
73
+ if (prev && !["INPUT", "TEXTAREA", "SELECT"].includes(prev.tagName)) {
74
+ const prevText = prev.textContent?.trim();
75
+ if (prevText && prevText.length < 60) return prevText;
76
+ }
77
+ const parent = el.parentElement;
78
+ if (parent) {
79
+ for (const child of Array.from(parent.children)) {
80
+ if (child === el) break;
81
+ if (["LABEL", "SPAN", "P", "H1", "H2", "H3", "H4", "H5", "H6", "LEGEND", "DIV"].includes(child.tagName)) {
82
+ const text = child.textContent?.trim();
83
+ if (text && text.length < 60 && !child.querySelector("input, textarea, select")) {
84
+ return text;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ if (el.id) return el.id.replace(/[_\-]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim();
90
+ const name = el.getAttribute("name");
91
+ if (name) return name.replace(/[_\-[\]]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim();
92
+ return "unknown";
93
+ }
94
+ function detectFields(container) {
95
+ const elements = container.querySelectorAll(INPUT_SELECTORS);
96
+ const fields = [];
97
+ elements.forEach((el) => {
98
+ if (el.disabled) return;
99
+ if (el.readOnly) return;
100
+ if (el.offsetParent === null && el.type !== "hidden") return;
101
+ const isCustomDropdown = el.getAttribute("role") === "combobox" && !(el instanceof HTMLInputElement) || el.getAttribute("aria-haspopup") === "listbox";
102
+ if (isCustomDropdown) {
103
+ const listboxId = el.getAttribute("aria-controls");
104
+ let listbox = listboxId ? document.getElementById(listboxId) : null;
105
+ if (!listbox) {
106
+ listbox = el.parentElement?.querySelector("[role=listbox]") || null;
107
+ }
108
+ const options = [];
109
+ if (listbox) {
110
+ listbox.querySelectorAll("[role=option]").forEach((opt) => {
111
+ const text = opt.textContent?.trim();
112
+ if (text) options.push(text);
113
+ });
114
+ }
115
+ const buttonText = el.textContent?.trim() || "";
116
+ const looksLikePlaceholder = buttonText.toLowerCase().startsWith("select") || buttonText === "";
117
+ const field2 = {
118
+ element: el,
119
+ type: "select",
120
+ name: el.id || el.getAttribute("name") || "",
121
+ label: findLabel(el),
122
+ required: el.getAttribute("aria-required") === "true",
123
+ currentValue: looksLikePlaceholder ? "" : buttonText,
124
+ options: options.length > 0 ? options : void 0
125
+ };
126
+ fields.push(field2);
127
+ return;
128
+ }
129
+ const field = {
130
+ element: el,
131
+ type: el instanceof HTMLSelectElement ? "select" : el.type || "text",
132
+ name: el.name || el.id || "",
133
+ label: findLabel(el),
134
+ required: el.required || el.getAttribute("aria-required") === "true",
135
+ currentValue: el.value
136
+ };
137
+ if (el instanceof HTMLSelectElement) {
138
+ field.options = Array.from(el.options).filter((opt) => opt.value && !opt.disabled).map((opt) => opt.textContent?.trim() || opt.value);
139
+ }
140
+ if ("min" in el && el.min) {
141
+ field.min = el.min;
142
+ }
143
+ if ("max" in el && el.max) {
144
+ field.max = el.max;
145
+ }
146
+ if ("pattern" in el && el.pattern) {
147
+ field.pattern = el.pattern;
148
+ }
149
+ fields.push(field);
150
+ });
151
+ return fields;
152
+ }
153
+ function describeFields(fields) {
154
+ return fields.map((f, i) => {
155
+ let desc = `[${i}] "${f.label}" (type: ${f.type}`;
156
+ if (f.required) desc += ", required";
157
+ if (f.options?.length) desc += `, options: [${f.options.join(", ")}]`;
158
+ if (f.min) desc += `, min: ${f.min}`;
159
+ if (f.max) desc += `, max: ${f.max}`;
160
+ if (f.pattern) desc += `, pattern: ${f.pattern}`;
161
+ if (f.currentValue) desc += `, current: "${f.currentValue}"`;
162
+ desc += ")";
163
+ return desc;
164
+ }).join("\n");
165
+ }
166
+ function extractBlockContext(container) {
167
+ const parts = [];
168
+ const pageTitle = document.title;
169
+ if (pageTitle) parts.push(`Page: ${pageTitle}`);
170
+ const headings = container.querySelectorAll("h1, h2, h3, h4, h5, h6");
171
+ headings.forEach((h) => {
172
+ const text = h.textContent?.trim();
173
+ if (text) parts.push(`Heading: ${text}`);
174
+ });
175
+ if (headings.length === 0) {
176
+ let prev = container;
177
+ while (prev) {
178
+ prev = prev.previousElementSibling;
179
+ if (prev && /^H[1-6]$/.test(prev.tagName)) {
180
+ const text = prev.textContent?.trim();
181
+ if (text) parts.push(`Section: ${text}`);
182
+ break;
183
+ }
184
+ }
185
+ const parent = container.closest("section, article, form, div[class]");
186
+ if (parent) {
187
+ const parentH = parent.querySelector("h1, h2, h3, h4, h5, h6");
188
+ if (parentH?.textContent?.trim()) {
189
+ parts.push(`Section: ${parentH.textContent.trim()}`);
190
+ }
191
+ }
192
+ }
193
+ const labels = container.querySelectorAll("label, legend, .label, p, span");
194
+ const seen = /* @__PURE__ */ new Set();
195
+ labels.forEach((el) => {
196
+ const text = el.textContent?.trim();
197
+ if (text && text.length > 2 && text.length < 100 && !seen.has(text)) {
198
+ seen.add(text);
199
+ parts.push(text);
200
+ }
201
+ });
202
+ return parts.slice(0, 20).join("\n");
203
+ }
204
+
205
+ // src/ai.ts
206
+ var SYSTEM_PROMPT = `You are a form-filling assistant. Given a list of form fields, page context, and an optional user prompt, generate realistic fake data to fill ALL fields.
207
+
208
+ Rules:
209
+ - Return ONLY a JSON object with a "fields" array of objects, each with "index" and "value" keys
210
+ - You MUST fill EVERY field \u2014 do not skip any
211
+ - Match the field type (email \u2192 valid email, phone \u2192 valid phone with country code, date \u2192 YYYY-MM-DD, datetime-local \u2192 YYYY-MM-DDTHH:MM, etc.)
212
+ - For select/dropdown fields: you MUST pick one of the listed options EXACTLY as written
213
+ - For checkboxes: add a "checked" boolean (true or false)
214
+ - For radio buttons: only fill one per group, use the option value
215
+ - Respect min/max constraints and patterns
216
+ - Generate contextually coherent data (same person's name, matching city/state/zip, etc.)
217
+ - Use the page context to infer what kind of data makes sense (e.g. a "Create Project" form \u2192 project-related data)
218
+ - If no user prompt is given, infer appropriate data from the field labels and page context
219
+ - Do NOT wrap the JSON in markdown code blocks \u2014 return raw JSON only`;
220
+ async function generateFillData(fields, userPrompt, settings, systemPrompt, blockContext) {
221
+ const fieldDescription = describeFields(fields);
222
+ const provider = PROVIDERS[settings.provider] || PROVIDERS.openai;
223
+ let userContent = `Form fields:
224
+ ${fieldDescription}`;
225
+ if (blockContext) {
226
+ userContent += `
227
+
228
+ Page context:
229
+ ${blockContext}`;
230
+ }
231
+ if (userPrompt) {
232
+ userContent += `
233
+
234
+ User instructions: ${userPrompt}`;
235
+ } else {
236
+ userContent += `
237
+
238
+ No specific instructions \u2014 generate realistic, contextually appropriate data for all fields.`;
239
+ }
240
+ const messages = [
241
+ {
242
+ role: "system",
243
+ content: systemPrompt ? `${systemPrompt}
244
+
245
+ ${SYSTEM_PROMPT}` : SYSTEM_PROMPT
246
+ },
247
+ {
248
+ role: "user",
249
+ content: userContent
250
+ }
251
+ ];
252
+ const body = {
253
+ model: provider.model,
254
+ messages,
255
+ temperature: 0.7
256
+ };
257
+ if (settings.provider === "openai") {
258
+ body.response_format = { type: "json_object" };
259
+ }
260
+ const response = await fetch(`${provider.baseURL}/chat/completions`, {
261
+ method: "POST",
262
+ headers: {
263
+ "Content-Type": "application/json",
264
+ Authorization: `Bearer ${settings.apiKey}`
265
+ },
266
+ body: JSON.stringify(body)
267
+ });
268
+ if (!response.ok) {
269
+ const error = await response.text();
270
+ throw new Error(`API error (${response.status}): ${error}`);
271
+ }
272
+ const data = await response.json();
273
+ const content = data.choices?.[0]?.message?.content;
274
+ if (!content) {
275
+ throw new Error("No content in API response");
276
+ }
277
+ const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, content];
278
+ const jsonStr = jsonMatch[1].trim();
279
+ const parsed = JSON.parse(jsonStr);
280
+ const arr = Array.isArray(parsed) ? parsed : parsed.fields || parsed.data || parsed.items || [];
281
+ if (!Array.isArray(arr)) {
282
+ throw new Error("AI response is not an array of field fills");
283
+ }
284
+ return arr;
285
+ }
286
+
287
+ // src/faker.ts
288
+ var FIRST_NAMES = ["James", "Sarah", "Michael", "Emma", "Robert", "Olivia", "David", "Sophia", "Daniel", "Isabella", "Ahmed", "Fatima", "Carlos", "Yuki", "Priya"];
289
+ var LAST_NAMES = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Wilson", "Anderson", "Taylor", "Thomas", "Moore", "Martin", "Lee"];
290
+ var COMPANIES = ["Acme Corp", "TechVault Inc", "Nexus Solutions", "Blue Ridge Systems", "Summit Digital", "CloudPeak Ltd", "DataForge Labs", "Apex Industries"];
291
+ var CITIES = ["New York", "San Francisco", "Chicago", "Austin", "Seattle", "Boston", "Denver", "Portland"];
292
+ var STREETS = ["123 Oak Street", "456 Maple Ave", "789 Pine Road", "321 Elm Blvd", "654 Cedar Lane", "987 Birch Drive"];
293
+ var DOMAINS = ["gmail.com", "outlook.com", "yahoo.com", "company.com", "example.com"];
294
+ var LOREM = ["Project planning and requirements gathering", "Implementation of core features", "Stakeholder review session", "Technical design workshop", "Sprint retrospective and planning", "Customer onboarding process", "Data migration strategy", "Integration testing phase"];
295
+ var TITLES = ["Discovery Workshop", "Requirements Review", "Design Sprint", "Technical Assessment", "Strategy Session", "Kick-off Meeting", "UAT Planning", "Go-Live Preparation"];
296
+ function pick(arr) {
297
+ return arr[Math.floor(Math.random() * arr.length)];
298
+ }
299
+ function randInt(min, max) {
300
+ return Math.floor(Math.random() * (max - min + 1)) + min;
301
+ }
302
+ function randDate(futureMonths = 6) {
303
+ const now = /* @__PURE__ */ new Date();
304
+ const future = new Date(now.getTime() + randInt(1, futureMonths * 30) * 864e5);
305
+ return future.toISOString().slice(0, 10);
306
+ }
307
+ function randDateTime(futureMonths = 6) {
308
+ const now = /* @__PURE__ */ new Date();
309
+ const future = new Date(now.getTime() + randInt(1, futureMonths * 30) * 864e5);
310
+ future.setHours(randInt(8, 18), randInt(0, 3) * 15, 0);
311
+ return future.toISOString().slice(0, 16);
312
+ }
313
+ function generateForField(field, context) {
314
+ const label = field.label.toLowerCase();
315
+ if (field.type === "select") {
316
+ if (field.options?.length) return pick(field.options);
317
+ return "__FIRST__";
318
+ }
319
+ if (field.type === "date") return randDate();
320
+ if (field.type === "datetime-local") return randDateTime();
321
+ if (field.type === "time") return `${String(randInt(8, 18)).padStart(2, "0")}:${String(randInt(0, 3) * 15).padStart(2, "0")}`;
322
+ if (field.type === "month") {
323
+ const d = /* @__PURE__ */ new Date();
324
+ d.setMonth(d.getMonth() + randInt(0, 6));
325
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
326
+ }
327
+ if (field.type === "email" || label.includes("email")) {
328
+ return context.email;
329
+ }
330
+ if (field.type === "tel" || label.includes("phone") || label.includes("mobile")) {
331
+ return `+1${randInt(200, 999)}${randInt(100, 999)}${randInt(1e3, 9999)}`;
332
+ }
333
+ if (field.type === "url" || label.includes("website") || label.includes("url")) {
334
+ return `https://www.${context.company.toLowerCase().replace(/\s+/g, "")}.com`;
335
+ }
336
+ if (field.type === "number" || field.type === "range") {
337
+ const min = field.min ? parseInt(field.min) : 1;
338
+ const max = field.max ? parseInt(field.max) : 100;
339
+ return String(randInt(min, max));
340
+ }
341
+ if (label.includes("first name") || label.includes("firstname")) return context.firstName;
342
+ if (label.includes("last name") || label.includes("lastname") || label.includes("surname")) return context.lastName;
343
+ if (label.includes("full name") || label === "name") return `${context.firstName} ${context.lastName}`;
344
+ if (label.includes("company") || label.includes("organization") || label.includes("business")) return context.company;
345
+ if (label.includes("address") || label.includes("street")) return pick(STREETS);
346
+ if (label.includes("city")) return pick(CITIES);
347
+ if (label.includes("state")) return pick(["California", "New York", "Texas", "Florida", "Washington"]);
348
+ if (label.includes("zip") || label.includes("postal")) return String(randInt(1e4, 99999));
349
+ if (label.includes("country")) return "United States";
350
+ if (label.includes("title") || label.includes("subject") || label.includes("name")) {
351
+ return pick(TITLES);
352
+ }
353
+ if (label.includes("job") || label.includes("role") || label.includes("position")) {
354
+ return pick(["Software Engineer", "Product Manager", "Business Analyst", "Solution Architect", "Project Manager"]);
355
+ }
356
+ if (label.includes("location") || label.includes("channel") || label.includes("venue")) {
357
+ return pick(["Microsoft Teams", "Zoom", "Google Meet", "On-site", "Conference Room A"]);
358
+ }
359
+ if (field.type === "textarea" || label.includes("description") || label.includes("note") || label.includes("comment") || label.includes("objective") || label.includes("goal")) {
360
+ return pick(LOREM);
361
+ }
362
+ if (field.type === "password") return "P@ssw0rd123!";
363
+ return pick([context.firstName, context.company, pick(TITLES)]);
364
+ }
365
+ function generateFakeData(fields) {
366
+ const firstName = pick(FIRST_NAMES);
367
+ const lastName = pick(LAST_NAMES);
368
+ const company = pick(COMPANIES);
369
+ const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@${pick(DOMAINS)}`;
370
+ const context = { firstName, lastName, email, company };
371
+ return fields.map((field, index) => {
372
+ if (field.type === "checkbox") {
373
+ return { index, value: "true", checked: Math.random() > 0.5 };
374
+ }
375
+ const value = generateForField(field, context);
376
+ return { index, value };
377
+ });
378
+ }
379
+
380
+ // src/filler.ts
381
+ function setNativeValue(el, value) {
382
+ const proto = el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
383
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
384
+ if (nativeSetter) {
385
+ nativeSetter.call(el, value);
386
+ } else {
387
+ el.value = value;
388
+ }
389
+ el.dispatchEvent(new Event("input", { bubbles: true }));
390
+ el.dispatchEvent(new Event("change", { bubbles: true }));
391
+ }
392
+ function focusBlur(el) {
393
+ el.focus();
394
+ el.dispatchEvent(new Event("blur", { bubbles: true }));
395
+ }
396
+ async function fillCustomSelect(button, value) {
397
+ button.click();
398
+ await new Promise((r) => setTimeout(r, 200));
399
+ const listboxId = button.getAttribute("aria-controls");
400
+ let listbox = listboxId ? document.getElementById(listboxId) : null;
401
+ if (!listbox) {
402
+ listbox = button.parentElement?.querySelector("[role=listbox]") || document.querySelector("[role=listbox]");
403
+ }
404
+ if (listbox) {
405
+ const options = listbox.querySelectorAll("[role=option]");
406
+ for (const opt of options) {
407
+ const text = opt.textContent?.trim();
408
+ if (text === value || text?.toLowerCase() === value.toLowerCase()) {
409
+ opt.click();
410
+ await new Promise((r) => setTimeout(r, 50));
411
+ return;
412
+ }
413
+ }
414
+ for (const opt of options) {
415
+ const text = opt.textContent?.trim()?.toLowerCase() || "";
416
+ if (text.includes(value.toLowerCase()) || value.toLowerCase().includes(text)) {
417
+ opt.click();
418
+ await new Promise((r) => setTimeout(r, 50));
419
+ return;
420
+ }
421
+ }
422
+ for (const opt of options) {
423
+ const text = (opt.textContent?.trim() || "").toLowerCase();
424
+ const isPlaceholder = text.startsWith("select") || text === "" || text === "---" || text === "choose" || text.startsWith("choose");
425
+ if (!isPlaceholder) {
426
+ opt.click();
427
+ await new Promise((r) => setTimeout(r, 50));
428
+ return;
429
+ }
430
+ }
431
+ if (options.length > 1) {
432
+ options[options.length - 1].click();
433
+ await new Promise((r) => setTimeout(r, 50));
434
+ return;
435
+ }
436
+ }
437
+ button.click();
438
+ }
439
+ async function fillFields(fields, fillData) {
440
+ let filled = 0;
441
+ const errors = [];
442
+ const filledRadioGroups = /* @__PURE__ */ new Set();
443
+ for (const item of fillData) {
444
+ const field = fields[item.index];
445
+ if (!field) {
446
+ errors.push(`Field index ${item.index} out of range`);
447
+ continue;
448
+ }
449
+ const el = field.element;
450
+ try {
451
+ if (field.type === "checkbox") {
452
+ const checkbox = el;
453
+ const shouldCheck = item.checked ?? item.value === "true";
454
+ if (checkbox.checked !== shouldCheck) {
455
+ const nativeCheckedSetter = Object.getOwnPropertyDescriptor(
456
+ HTMLInputElement.prototype,
457
+ "checked"
458
+ )?.set;
459
+ if (nativeCheckedSetter) {
460
+ nativeCheckedSetter.call(checkbox, shouldCheck);
461
+ } else {
462
+ checkbox.checked = shouldCheck;
463
+ }
464
+ checkbox.dispatchEvent(new Event("click", { bubbles: true }));
465
+ checkbox.dispatchEvent(new Event("input", { bubbles: true }));
466
+ checkbox.dispatchEvent(new Event("change", { bubbles: true }));
467
+ }
468
+ filled++;
469
+ } else if (field.type === "radio") {
470
+ const radio = el;
471
+ if (radio.name && filledRadioGroups.has(radio.name)) continue;
472
+ if (radio.name) {
473
+ const group = document.querySelectorAll(
474
+ `input[type="radio"][name="${CSS.escape(radio.name)}"]`
475
+ );
476
+ for (const r of group) {
477
+ if (r.value === item.value || findLabel2(r) === item.value) {
478
+ const nativeCheckedSetter = Object.getOwnPropertyDescriptor(
479
+ HTMLInputElement.prototype,
480
+ "checked"
481
+ )?.set;
482
+ if (nativeCheckedSetter) {
483
+ nativeCheckedSetter.call(r, true);
484
+ } else {
485
+ r.checked = true;
486
+ }
487
+ r.dispatchEvent(new Event("click", { bubbles: true }));
488
+ r.dispatchEvent(new Event("input", { bubbles: true }));
489
+ r.dispatchEvent(new Event("change", { bubbles: true }));
490
+ break;
491
+ }
492
+ }
493
+ filledRadioGroups.add(radio.name);
494
+ } else {
495
+ radio.checked = true;
496
+ radio.dispatchEvent(new Event("click", { bubbles: true }));
497
+ radio.dispatchEvent(new Event("change", { bubbles: true }));
498
+ }
499
+ filled++;
500
+ } else if (field.type === "select") {
501
+ if (el instanceof HTMLSelectElement) {
502
+ const pickFirst = () => {
503
+ const real = Array.from(el.options).find((o) => {
504
+ if (!o.value || o.disabled) return false;
505
+ const t = (o.textContent?.trim() || "").toLowerCase();
506
+ return !t.startsWith("select") && t !== "" && !t.startsWith("choose") && t !== "---";
507
+ });
508
+ if (real) setNativeValue(el, real.value);
509
+ };
510
+ if (item.value === "__FIRST__") {
511
+ pickFirst();
512
+ } else {
513
+ const option = Array.from(el.options).find(
514
+ (opt) => opt.textContent?.trim() === item.value || opt.value === item.value
515
+ );
516
+ if (option) {
517
+ setNativeValue(el, option.value);
518
+ } else {
519
+ pickFirst();
520
+ }
521
+ }
522
+ focusBlur(el);
523
+ } else {
524
+ await fillCustomSelect(el, item.value);
525
+ }
526
+ filled++;
527
+ } else {
528
+ setNativeValue(el, item.value);
529
+ focusBlur(el);
530
+ filled++;
531
+ }
532
+ const origBg = el.style.backgroundColor;
533
+ el.style.transition = "background-color 0.3s";
534
+ el.style.backgroundColor = "rgba(99, 102, 241, 0.12)";
535
+ setTimeout(() => {
536
+ el.style.backgroundColor = origBg;
537
+ }, 800);
538
+ } catch (err) {
539
+ errors.push(
540
+ `Failed to fill "${field.label}": ${err instanceof Error ? err.message : String(err)}`
541
+ );
542
+ }
543
+ }
544
+ return { filled, errors };
545
+ }
546
+ function findLabel2(el) {
547
+ if (el.id) {
548
+ const label = document.querySelector(
549
+ `label[for="${CSS.escape(el.id)}"]`
550
+ );
551
+ if (label?.textContent?.trim()) return label.textContent.trim();
552
+ }
553
+ const parent = el.closest("label");
554
+ if (parent) {
555
+ const clone = parent.cloneNode(true);
556
+ clone.querySelectorAll("input").forEach((c) => c.remove());
557
+ return clone.textContent?.trim() || "";
558
+ }
559
+ return "";
560
+ }
561
+
562
+ // src/selector.ts
563
+ var currentHighlight = null;
564
+ var overlay = null;
565
+ function createOverlay(color) {
566
+ const div = document.createElement("div");
567
+ div.id = "ghostfill-selector-overlay";
568
+ const r = parseInt(color.slice(1, 3), 16);
569
+ const g = parseInt(color.slice(3, 5), 16);
570
+ const b = parseInt(color.slice(5, 7), 16);
571
+ Object.assign(div.style, {
572
+ position: "fixed",
573
+ pointerEvents: "none",
574
+ border: `2px dashed ${color}`,
575
+ borderRadius: "4px",
576
+ backgroundColor: `rgba(${r}, ${g}, ${b}, 0.08)`,
577
+ zIndex: "2147483645",
578
+ transition: "all 0.15s ease"
579
+ });
580
+ document.body.appendChild(div);
581
+ return div;
582
+ }
583
+ function positionOverlay(el, color) {
584
+ if (!overlay) overlay = createOverlay(color);
585
+ const rect = el.getBoundingClientRect();
586
+ Object.assign(overlay.style, {
587
+ top: `${rect.top}px`,
588
+ left: `${rect.left}px`,
589
+ width: `${rect.width}px`,
590
+ height: `${rect.height}px`,
591
+ display: "block"
592
+ });
593
+ }
594
+ function hideOverlay() {
595
+ if (overlay) overlay.style.display = "none";
596
+ }
597
+ function removeOverlay() {
598
+ overlay?.remove();
599
+ overlay = null;
600
+ }
601
+ function findBestContainer(target) {
602
+ if (target.tagName === "FORM" || target.tagName === "FIELDSET") return target;
603
+ let el = target;
604
+ while (el) {
605
+ if (el.tagName === "FORM" || el.tagName === "FIELDSET") return el;
606
+ const inputs = el.querySelectorAll(
607
+ "input:not([type=hidden]):not([type=submit]):not([type=button]), textarea, select"
608
+ );
609
+ if (inputs.length >= 2) return el;
610
+ el = el.parentElement;
611
+ }
612
+ return target;
613
+ }
614
+ function startSelection(onSelect, onCancel, ghostfillRoot, highlightColor = "#6366f1") {
615
+ document.body.style.cursor = "crosshair";
616
+ function handleMouseMove(e) {
617
+ const target = e.target;
618
+ if (ghostfillRoot?.contains(target)) return;
619
+ if (target.id === "ghostfill-selector-overlay") return;
620
+ const container = findBestContainer(target);
621
+ if (container !== currentHighlight) {
622
+ currentHighlight = container;
623
+ positionOverlay(container, highlightColor);
624
+ }
625
+ }
626
+ function handleClick(e) {
627
+ const target = e.target;
628
+ if (ghostfillRoot?.contains(target)) return;
629
+ e.preventDefault();
630
+ e.stopPropagation();
631
+ cleanup();
632
+ const container = findBestContainer(target);
633
+ onSelect(container);
634
+ }
635
+ function handleKeyDown(e) {
636
+ if (e.key === "Escape") {
637
+ cleanup();
638
+ onCancel();
639
+ }
640
+ }
641
+ function cleanup() {
642
+ document.body.style.cursor = "";
643
+ document.removeEventListener("mousemove", handleMouseMove, true);
644
+ document.removeEventListener("click", handleClick, true);
645
+ document.removeEventListener("keydown", handleKeyDown, true);
646
+ hideOverlay();
647
+ removeOverlay();
648
+ currentHighlight = null;
649
+ }
650
+ document.addEventListener("mousemove", handleMouseMove, true);
651
+ document.addEventListener("click", handleClick, true);
652
+ document.addEventListener("keydown", handleKeyDown, true);
653
+ return cleanup;
654
+ }
655
+
656
+ // src/overlay.ts
657
+ var STORAGE_KEY = "ghostfill_settings";
658
+ var POS_KEY = "ghostfill_pos";
659
+ var FAB_POS_KEY = "ghostfill_fab_pos";
660
+ function loadSettings() {
661
+ try {
662
+ const raw = localStorage.getItem(STORAGE_KEY);
663
+ if (raw) return JSON.parse(raw);
664
+ } catch {
665
+ }
666
+ return { apiKey: "", provider: "openai", highlightColor: "#6366f1", theme: "dark", useAI: false, presets: [], activePresetId: null };
667
+ }
668
+ function saveSettings(s) {
669
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
670
+ }
671
+ function loadPosition() {
672
+ try {
673
+ const raw = localStorage.getItem(POS_KEY);
674
+ if (raw) return JSON.parse(raw);
675
+ } catch {
676
+ }
677
+ return null;
678
+ }
679
+ function savePosition(x, y) {
680
+ localStorage.setItem(POS_KEY, JSON.stringify({ x, y }));
681
+ }
682
+ function cleanError(err) {
683
+ const raw = err instanceof Error ? err.message : String(err);
684
+ const match = raw.match(/"message"\s*:\s*"([^"]+)"/);
685
+ if (match) return match[1];
686
+ const stripped = raw.replace(/^OpenAI API error \(\d+\):\s*/, "");
687
+ try {
688
+ const parsed = JSON.parse(stripped);
689
+ if (parsed?.error?.message) return parsed.error.message;
690
+ } catch {
691
+ }
692
+ return stripped.length > 80 ? stripped.slice(0, 80) + "..." : stripped;
693
+ }
694
+ var ICONS = {
695
+ select: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
696
+ <path d="M12 2v4M12 18v4M2 12h4M18 12h4"/>
697
+ <circle cx="12" cy="12" r="4"/>
698
+ </svg>`,
699
+ sparkles: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
700
+ <path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>
701
+ <path d="M20 3v4M22 5h-4"/>
702
+ </svg>`,
703
+ settings: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
704
+ <circle cx="12" cy="12" r="3"/>
705
+ <path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"/>
706
+ </svg>`,
707
+ close: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
708
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
709
+ </svg>`,
710
+ ghost: `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
711
+ <path d="M12 2C7.58 2 4 5.58 4 10v10l2-2 2 2 2-2 2 2 2-2 2 2 2-2 2 2V10c0-4.42-3.58-8-8-8z"/>
712
+ <circle cx="9" cy="10" r="1.5" fill="currentColor"/>
713
+ <circle cx="15" cy="10" r="1.5" fill="currentColor"/>
714
+ </svg>`,
715
+ spinner: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
716
+ <path d="M12 2a10 10 0 0 1 10 10"/>
717
+ </svg>`,
718
+ // Field type icons (14x14)
719
+ ftText: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7V4h16v3M9 20h6M12 4v16"/></svg>`,
720
+ ftEmail: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>`,
721
+ ftPhone: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z"/></svg>`,
722
+ ftNumber: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 9h16M4 15h16M10 3L8 21M16 3l-2 18"/></svg>`,
723
+ ftDate: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>`,
724
+ ftSelect: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>`,
725
+ ftTextarea: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 10H3M21 6H3M21 14H3M17 18H3"/></svg>`,
726
+ ftCheckbox: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 12l2 2 4-4"/></svg>`,
727
+ ftRadio: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4" fill="currentColor"/></svg>`,
728
+ ftUrl: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>`,
729
+ ftPassword: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>`,
730
+ ftFile: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>`,
731
+ sun: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>`,
732
+ moon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>`
733
+ };
734
+ var CSS2 = `
735
+ :host {
736
+ all: initial;
737
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
738
+ font-size: 13px;
739
+ color: #e4e4e7;
740
+ line-height: 1.4;
741
+ }
742
+ * { box-sizing: border-box; margin: 0; padding: 0; }
743
+
744
+ .gf-bar {
745
+ position: fixed;
746
+ z-index: 2147483646;
747
+ display: flex;
748
+ align-items: center;
749
+ gap: 2px;
750
+ background: #18181b;
751
+ border-radius: 14px;
752
+ padding: 5px 6px;
753
+ box-shadow: 0 8px 32px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06);
754
+ user-select: none;
755
+ cursor: grab;
756
+ pointer-events: auto;
757
+ }
758
+ .gf-bar.dragging { cursor: grabbing; opacity: 0.9; }
759
+
760
+ .gf-bar-btn {
761
+ position: relative;
762
+ width: 36px;
763
+ height: 36px;
764
+ border: none;
765
+ border-radius: 10px;
766
+ background: transparent;
767
+ color: #a1a1aa;
768
+ cursor: pointer;
769
+ display: flex;
770
+ align-items: center;
771
+ justify-content: center;
772
+ transition: background 0.15s, color 0.15s;
773
+ }
774
+ .gf-bar-btn:hover { background: #27272a; color: #fafafa; }
775
+ .gf-bar-btn.active { background: #3f3f46; color: #fafafa; }
776
+ .gf-bar-btn:disabled { opacity: 0.35; cursor: not-allowed; }
777
+ .gf-bar-btn:disabled:hover { background: transparent; color: #a1a1aa; }
778
+
779
+ .gf-divider {
780
+ width: 1px; height: 20px; background: #3f3f46; margin: 0 4px; flex-shrink: 0;
781
+ }
782
+
783
+
784
+ .gf-fab {
785
+ position: fixed; z-index: 2147483646;
786
+ width: 44px; height: 44px; border-radius: 50%; border: none;
787
+ background: #18181b; color: #a1a1aa; cursor: grab;
788
+ display: none; align-items: center; justify-content: center;
789
+ pointer-events: auto;
790
+ box-shadow: 0 4px 16px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06);
791
+ transition: transform 0.15s, color 0.15s;
792
+ }
793
+ .gf-fab:hover { color: #a78bfa; }
794
+ .gf-fab:hover > svg { animation: gf-float 1.2s ease-in-out infinite; }
795
+ .gf-fab.visible { display: flex; }
796
+ @keyframes gf-float {
797
+ 0%, 100% { transform: translateY(0) rotate(0deg); }
798
+ 25% { transform: translateY(-2px) rotate(-5deg); }
799
+ 75% { transform: translateY(1px) rotate(5deg); }
800
+ }
801
+
802
+ .gf-popover {
803
+ position: fixed; z-index: 2147483646;
804
+ background: #1a1a1a; border-radius: 16px; pointer-events: auto;
805
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08);
806
+ display: none; flex-direction: column; overflow: hidden;
807
+ min-width: 280px;
808
+ }
809
+ .gf-popover.open { display: flex; }
810
+
811
+ .gf-pop-header {
812
+ display: flex; align-items: center; justify-content: space-between;
813
+ min-height: 24px; padding: 13px 16px 0;
814
+ margin-bottom: 8px; padding-bottom: 9px;
815
+ border-bottom: 1px solid rgba(255,255,255,0.07);
816
+ }
817
+ .gf-pop-header h3 {
818
+ font-size: 13px; font-weight: 600; color: #fff;
819
+ letter-spacing: -0.0094em;
820
+ }
821
+ .gf-pop-header .gf-slash { color: rgba(255,255,255,0.5); }
822
+ .gf-pop-header .gf-header-right {
823
+ display: flex; align-items: center; gap: 6px;
824
+ }
825
+ .gf-pop-header .gf-version {
826
+ font-size: 11px; font-weight: 400; color: rgba(255,255,255,0.4);
827
+ letter-spacing: -0.0094em;
828
+ }
829
+ .gf-theme-btn {
830
+ width: 20px; height: 20px; border: none; border-radius: 4px;
831
+ background: transparent; color: rgba(255,255,255,0.4); cursor: pointer;
832
+ display: flex; align-items: center; justify-content: center;
833
+ transition: color 0.15s;
834
+ }
835
+ .gf-theme-btn:hover { color: rgba(255,255,255,0.7); }
836
+ .gf-theme-btn svg { width: 14px; height: 14px; }
837
+
838
+ .gf-sep {
839
+ height: 1px; background: rgba(255,255,255,0.07);
840
+ margin: 8px 0 10px;
841
+ }
842
+
843
+ /* Highlight color picker */
844
+ .gf-colors {
845
+ display: flex; gap: 6px; flex-wrap: wrap;
846
+ }
847
+ .gf-color-dot {
848
+ width: 24px; height: 24px; border-radius: 50%; border: 2px solid transparent;
849
+ cursor: pointer; transition: border-color 0.15s, transform 0.1s;
850
+ }
851
+ .gf-color-dot:hover { transform: scale(1.1); }
852
+ .gf-color-dot.selected { border-color: #fafafa; }
853
+
854
+ .gf-pop-body {
855
+ padding: 0 16px 16px; display: flex; flex-direction: column; gap: 10px;
856
+ max-height: 400px; overflow-y: auto;
857
+ }
858
+
859
+ .gf-field { display: flex; flex-direction: column; gap: 4px; }
860
+ .gf-label {
861
+ font-size: 13px; font-weight: 400; color: rgba(255,255,255,0.5);
862
+ letter-spacing: -0.0094em; display: flex; align-items: center; gap: 2px;
863
+ }
864
+ .gf-input {
865
+ width: 100%; padding: 6px 8px; background: rgba(255,255,255,0.06);
866
+ border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff;
867
+ font-family: inherit; font-size: 13px; outline: none; transition: border-color 0.15s;
868
+ }
869
+ .gf-input:focus { border-color: #6366f1; box-shadow: 0 0 0 2px rgba(99,102,241,0.15); }
870
+ .gf-input::placeholder { color: rgba(255,255,255,0.25); }
871
+ .gf-input-mono { font-family: "SF Mono", "Fira Code", monospace; font-size: 12px; }
872
+
873
+ textarea.gf-input { min-height: 56px; resize: vertical; line-height: 1.5; }
874
+
875
+ .gf-save-btn, .gf-fill-btn {
876
+ width: 100%; padding: 7px; border: none; border-radius: 8px;
877
+ background: #6366f1; color: white; font-family: inherit;
878
+ font-size: 13px; font-weight: 500; cursor: pointer; transition: background 0.15s;
879
+ display: flex; align-items: center; justify-content: center; gap: 5px;
880
+ }
881
+ .gf-save-btn:hover, .gf-fill-btn:hover { background: #4f46e5; }
882
+ .gf-fill-btn:disabled { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.25); cursor: not-allowed; }
883
+
884
+ .gf-pop-body::-webkit-scrollbar { width: 6px; }
885
+ .gf-pop-body::-webkit-scrollbar-track { background: transparent; }
886
+ .gf-pop-body::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
887
+
888
+ .gf-field-count { font-size: 11px; color: #71717a; }
889
+
890
+ /* \u2500\u2500 Field cards grid \u2500\u2500 */
891
+ .gf-fields-grid {
892
+ display: flex; flex-wrap: wrap; gap: 6px;
893
+ max-height: 140px; overflow-y: auto; padding: 2px;
894
+ scrollbar-color: #3f3f46 transparent;
895
+ }
896
+ .gf-fields-grid::-webkit-scrollbar { width: 5px; }
897
+ .gf-fields-grid::-webkit-scrollbar-track { background: transparent; }
898
+ .gf-fields-grid::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
899
+
900
+ .gf-field-card {
901
+ display: flex; align-items: center; gap: 5px;
902
+ padding: 4px 8px; border-radius: 8px;
903
+ background: #27272a; border: 1px solid #3f3f46;
904
+ cursor: default; transition: border-color 0.15s, background 0.15s;
905
+ max-width: 100%;
906
+ }
907
+ .gf-field-card:hover {
908
+ border-color: #6366f1; background: #2e2442;
909
+ }
910
+ .gf-field-card .gf-fc-icon {
911
+ flex-shrink: 0; width: 16px; height: 16px; color: #6366f1;
912
+ display: flex; align-items: center; justify-content: center;
913
+ }
914
+ .gf-field-card .gf-fc-icon svg { width: 14px; height: 14px; }
915
+ .gf-field-card .gf-fc-label {
916
+ font-size: 11px; color: #d4d4d8; white-space: nowrap;
917
+ overflow: hidden; text-overflow: ellipsis;
918
+ }
919
+ .gf-field-card .gf-fc-req {
920
+ color: #f87171; font-size: 11px; flex-shrink: 0;
921
+ }
922
+
923
+ /* Inline status text inside prompt popover */
924
+ .gf-status {
925
+ font-size: 11px; padding: 4px 0; text-align: center; min-height: 18px;
926
+ transition: color 0.15s;
927
+ }
928
+ .gf-status.error { color: #f87171; }
929
+ .gf-status.success { color: #4ade80; }
930
+ .gf-status.info { color: #71717a; }
931
+
932
+ .gf-spin { animation: gf-spin 0.7s linear infinite; }
933
+ @keyframes gf-spin { to { transform: rotate(360deg); } }
934
+
935
+ .gf-badge {
936
+ position: absolute; top: 2px; right: 2px; width: 14px; height: 14px;
937
+ border-radius: 7px; background: #6366f1; color: white;
938
+ font-size: 9px; font-weight: 700; display: flex; align-items: center;
939
+ justify-content: center; line-height: 1;
940
+ }
941
+
942
+ .gf-dot-warn {
943
+ position: absolute; top: 4px; right: 4px; width: 6px; height: 6px;
944
+ border-radius: 3px; background: #f59e0b;
945
+ }
946
+
947
+ /* Toggle switch */
948
+ .gf-toggle {
949
+ position: relative; display: inline-block; width: 32px; height: 18px; cursor: pointer;
950
+ }
951
+ .gf-toggle input { opacity: 0; width: 0; height: 0; }
952
+ .gf-toggle-slider {
953
+ position: absolute; inset: 0; background: #3f3f46; border-radius: 9px;
954
+ transition: background 0.2s;
955
+ }
956
+ .gf-toggle-slider::before {
957
+ content: ""; position: absolute; left: 2px; top: 2px;
958
+ width: 14px; height: 14px; border-radius: 50%; background: #fafafa;
959
+ transition: transform 0.2s;
960
+ }
961
+ .gf-toggle input:checked + .gf-toggle-slider { background: #6366f1; }
962
+ .gf-toggle input:checked + .gf-toggle-slider::before { transform: translateX(14px); }
963
+
964
+ /* Custom inline picker (like Agentation's "Standard") */
965
+ .gf-picker {
966
+ position: relative; display: flex; align-items: center; gap: 4px;
967
+ cursor: pointer; user-select: none;
968
+ }
969
+ .gf-picker-value {
970
+ font-size: 13px; font-weight: 400; color: rgba(255,255,255,0.85);
971
+ letter-spacing: -0.0094em;
972
+ }
973
+ .gf-picker-dots {
974
+ font-size: 14px; color: #52525b; line-height: 1;
975
+ }
976
+ .gf-picker-menu {
977
+ display: none; position: absolute; right: 0; top: calc(100% + 6px);
978
+ background: #09090b; border: 1px solid #27272a; border-radius: 8px;
979
+ padding: 4px 0; min-width: 140px; z-index: 10;
980
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
981
+ }
982
+ .gf-picker-menu.open { display: block; }
983
+ .gf-picker-option {
984
+ padding: 6px 12px; font-size: 12px; color: #a1a1aa; cursor: pointer;
985
+ transition: background 0.1s, color 0.1s;
986
+ }
987
+ .gf-picker-option:hover { background: #27272a; color: #fafafa; }
988
+ .gf-picker-option.selected { color: #6366f1; }
989
+
990
+ /* Preset chips */
991
+ .gf-presets-row {
992
+ display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
993
+ }
994
+ .gf-preset-chip {
995
+ padding: 3px 8px; border-radius: 10px; font-size: 11px; font-weight: 500;
996
+ background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.5);
997
+ cursor: pointer; border: 1px solid transparent;
998
+ transition: all 0.15s; white-space: nowrap;
999
+ }
1000
+ .gf-preset-chip:hover { color: rgba(255,255,255,0.8); background: rgba(255,255,255,0.1); }
1001
+ .gf-preset-chip.active { border-color: #6366f1; color: #a5b4fc; background: rgba(99,102,241,0.12); }
1002
+ .gf-preset-chip.add {
1003
+ color: rgba(255,255,255,0.3); border: 1px dashed rgba(255,255,255,0.15);
1004
+ background: transparent;
1005
+ }
1006
+ .gf-preset-chip.add:hover { color: rgba(255,255,255,0.6); border-color: rgba(255,255,255,0.3); }
1007
+
1008
+ /* Preset list in settings */
1009
+ .gf-preset-list { display: flex; flex-direction: column; gap: 4px; }
1010
+ .gf-preset-item {
1011
+ display: flex; align-items: center; justify-content: space-between;
1012
+ padding: 4px 8px; border-radius: 6px; background: rgba(255,255,255,0.04);
1013
+ }
1014
+ .gf-preset-item-name { font-size: 12px; color: rgba(255,255,255,0.7); }
1015
+ .gf-preset-del {
1016
+ background: none; border: none; color: rgba(255,255,255,0.25); cursor: pointer;
1017
+ font-size: 14px; padding: 0 2px; line-height: 1; transition: color 0.15s;
1018
+ }
1019
+ .gf-preset-del:hover { color: #f87171; }
1020
+
1021
+ /* Preset add form */
1022
+ .gf-preset-form { display: flex; flex-direction: column; gap: 6px; }
1023
+ .gf-preset-form-row { display: flex; gap: 4px; }
1024
+ .gf-preset-form-row .gf-input { flex: 1; }
1025
+ .gf-preset-form-actions { display: flex; gap: 4px; justify-content: flex-end; }
1026
+ .gf-preset-form-btn {
1027
+ padding: 3px 10px; border: none; border-radius: 6px; font-size: 11px;
1028
+ cursor: pointer; font-family: inherit; transition: background 0.15s;
1029
+ }
1030
+ .gf-preset-form-btn.save { background: #6366f1; color: white; }
1031
+ .gf-preset-form-btn.save:hover { background: #4f46e5; }
1032
+ .gf-preset-form-btn.cancel { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.5); }
1033
+ .gf-preset-form-btn.cancel:hover { background: rgba(255,255,255,0.1); }
1034
+
1035
+ /* Help badge */
1036
+ .gf-help {
1037
+ display: inline-flex; align-items: center; justify-content: center;
1038
+ width: 14px; height: 14px; border-radius: 50%;
1039
+ background: #3f3f46; color: #a1a1aa; font-size: 9px; font-weight: 700;
1040
+ cursor: help; flex-shrink: 0;
1041
+ }
1042
+ `;
1043
+ function createOverlay2(options) {
1044
+ const saved = loadSettings();
1045
+ if (options.apiKey && !saved.apiKey) {
1046
+ saved.apiKey = options.apiKey;
1047
+ saveSettings(saved);
1048
+ }
1049
+ if (options.model && !saved.model) saved.model = options.model;
1050
+ if (options.baseURL && !saved.baseURL) saved.baseURL = options.baseURL;
1051
+ const host = document.createElement("div");
1052
+ host.id = "ghostfill-root";
1053
+ host.style.cssText = "display:contents;";
1054
+ document.body.appendChild(host);
1055
+ const shadow = host.attachShadow({ mode: "open" });
1056
+ const style = document.createElement("style");
1057
+ style.textContent = CSS2;
1058
+ shadow.appendChild(style);
1059
+ const state = {
1060
+ active: false,
1061
+ selecting: false,
1062
+ selectedBlock: null,
1063
+ fields: [],
1064
+ overlay: host,
1065
+ shadowRoot: shadow
1066
+ };
1067
+ const bar = document.createElement("div");
1068
+ bar.className = "gf-bar";
1069
+ shadow.appendChild(bar);
1070
+ bar.style.display = "none";
1071
+ const savedPos = loadPosition();
1072
+ if (savedPos) {
1073
+ bar.style.left = `${savedPos.x}px`;
1074
+ bar.style.top = `${savedPos.y}px`;
1075
+ } else {
1076
+ bar.style.bottom = "20px";
1077
+ bar.style.left = "50%";
1078
+ bar.style.transform = "translateX(-50%)";
1079
+ }
1080
+ function makeBtn(icon) {
1081
+ const btn = document.createElement("button");
1082
+ btn.className = "gf-bar-btn";
1083
+ btn.innerHTML = icon;
1084
+ return btn;
1085
+ }
1086
+ const btnSelect = makeBtn(ICONS.select);
1087
+ const btnFill = makeBtn(ICONS.sparkles);
1088
+ const btnSettings = makeBtn(ICONS.settings);
1089
+ const btnMinimize = makeBtn(ICONS.close);
1090
+ btnFill.disabled = true;
1091
+ const badge = document.createElement("span");
1092
+ badge.className = "gf-badge";
1093
+ badge.style.display = "none";
1094
+ btnSelect.style.position = "relative";
1095
+ btnSelect.appendChild(badge);
1096
+ const dotWarn = document.createElement("span");
1097
+ dotWarn.className = "gf-dot-warn";
1098
+ btnSettings.style.position = "relative";
1099
+ btnSettings.appendChild(dotWarn);
1100
+ if (!saved.useAI || saved.apiKey) dotWarn.style.display = "none";
1101
+ const divider1 = document.createElement("span");
1102
+ divider1.className = "gf-divider";
1103
+ const divider2 = document.createElement("span");
1104
+ divider2.className = "gf-divider";
1105
+ bar.append(btnSelect, btnFill, divider1, btnSettings, divider2, btnMinimize);
1106
+ const fab = document.createElement("button");
1107
+ fab.className = "gf-fab visible";
1108
+ fab.innerHTML = ICONS.ghost;
1109
+ fab.title = "GhostFill";
1110
+ const savedFabPos = (() => {
1111
+ try {
1112
+ const raw = localStorage.getItem(FAB_POS_KEY);
1113
+ if (raw) return JSON.parse(raw);
1114
+ } catch {
1115
+ }
1116
+ return null;
1117
+ })();
1118
+ if (savedFabPos) {
1119
+ const x = Math.min(savedFabPos.x, window.innerWidth - 60);
1120
+ const y = Math.min(savedFabPos.y, window.innerHeight - 60);
1121
+ fab.style.left = `${Math.max(8, x)}px`;
1122
+ fab.style.top = `${Math.max(8, y)}px`;
1123
+ } else {
1124
+ fab.style.right = "80px";
1125
+ fab.style.bottom = "80px";
1126
+ }
1127
+ shadow.appendChild(fab);
1128
+ function positionFab() {
1129
+ const savedFab = (() => {
1130
+ try {
1131
+ const raw = localStorage.getItem(FAB_POS_KEY);
1132
+ if (raw) return JSON.parse(raw);
1133
+ } catch {
1134
+ }
1135
+ return null;
1136
+ })();
1137
+ if (savedFab) {
1138
+ fab.style.left = `${savedFab.x}px`;
1139
+ fab.style.top = `${savedFab.y}px`;
1140
+ } else {
1141
+ const barRect = bar.getBoundingClientRect();
1142
+ fab.style.left = `${barRect.left + barRect.width / 2 - 22}px`;
1143
+ fab.style.top = `${barRect.top + barRect.height / 2 - 22}px`;
1144
+ }
1145
+ fab.style.bottom = "";
1146
+ fab.style.right = "";
1147
+ }
1148
+ let isDragging = false;
1149
+ let dragStartX = 0;
1150
+ let dragStartY = 0;
1151
+ let barStartX = 0;
1152
+ let barStartY = 0;
1153
+ let hasDragged = false;
1154
+ function onDragStart(e) {
1155
+ isDragging = true;
1156
+ hasDragged = false;
1157
+ dragStartX = e.clientX;
1158
+ dragStartY = e.clientY;
1159
+ const rect = bar.getBoundingClientRect();
1160
+ barStartX = rect.left;
1161
+ barStartY = rect.top;
1162
+ bar.style.transform = "none";
1163
+ bar.style.bottom = "";
1164
+ document.addEventListener("mousemove", onDragMove);
1165
+ document.addEventListener("mouseup", onDragEnd);
1166
+ }
1167
+ function onDragMove(e) {
1168
+ if (!isDragging) return;
1169
+ const dx = e.clientX - dragStartX;
1170
+ const dy = e.clientY - dragStartY;
1171
+ if (!hasDragged && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
1172
+ hasDragged = true;
1173
+ bar.classList.add("dragging");
1174
+ }
1175
+ if (!hasDragged) return;
1176
+ const barW = bar.offsetWidth;
1177
+ const barH = bar.offsetHeight;
1178
+ bar.style.left = `${Math.max(0, Math.min(window.innerWidth - barW, barStartX + dx))}px`;
1179
+ bar.style.top = `${Math.max(0, Math.min(window.innerHeight - barH, barStartY + dy))}px`;
1180
+ }
1181
+ function onDragEnd() {
1182
+ isDragging = false;
1183
+ bar.classList.remove("dragging");
1184
+ document.removeEventListener("mousemove", onDragMove);
1185
+ document.removeEventListener("mouseup", onDragEnd);
1186
+ if (hasDragged) {
1187
+ const rect = bar.getBoundingClientRect();
1188
+ savePosition(rect.left, rect.top);
1189
+ repositionPopover();
1190
+ setTimeout(() => {
1191
+ hasDragged = false;
1192
+ }, 0);
1193
+ }
1194
+ }
1195
+ bar.addEventListener("mousedown", onDragStart);
1196
+ const HIGHLIGHT_COLORS = [
1197
+ { color: "#8b5cf6", name: "Purple" },
1198
+ { color: "#3b82f6", name: "Blue" },
1199
+ { color: "#06b6d4", name: "Cyan" },
1200
+ { color: "#22c55e", name: "Green" },
1201
+ { color: "#eab308", name: "Yellow" },
1202
+ { color: "#f97316", name: "Orange" },
1203
+ { color: "#ef4444", name: "Red" }
1204
+ ];
1205
+ const settingsPop = document.createElement("div");
1206
+ settingsPop.className = "gf-popover";
1207
+ settingsPop.innerHTML = `
1208
+ <div class="gf-pop-header">
1209
+ <h3><span class="gf-slash">/</span>ghostfill</h3>
1210
+ <div class="gf-header-right">
1211
+ <span class="gf-version">v0.1.0</span>
1212
+ <button class="gf-theme-btn" id="gf-s-theme" title="Toggle theme">
1213
+ ${saved.theme === "dark" ? ICONS.sun : ICONS.moon}
1214
+ </button>
1215
+ </div>
1216
+ </div>
1217
+ <div class="gf-pop-body">
1218
+ <div class="gf-field">
1219
+ <label class="gf-label">Highlight Colour</label>
1220
+ <div class="gf-colors" id="gf-s-colors">
1221
+ ${HIGHLIGHT_COLORS.map(
1222
+ (c) => `<div class="gf-color-dot${saved.highlightColor === c.color ? " selected" : ""}" data-color="${c.color}" style="background:${c.color}" title="${c.name}"></div>`
1223
+ ).join("")}
1224
+ </div>
1225
+ </div>
1226
+ <div class="gf-sep"></div>
1227
+ <div class="gf-field" style="flex-direction:row;align-items:center;justify-content:space-between">
1228
+ <label class="gf-label" style="margin:0">Use AI</label>
1229
+ <label class="gf-toggle">
1230
+ <input type="checkbox" id="gf-s-useai" ${saved.useAI ? "checked" : ""} />
1231
+ <span class="gf-toggle-slider"></span>
1232
+ </label>
1233
+ </div>
1234
+ <div id="gf-s-ai-section" style="display:${saved.useAI ? "flex" : "none"};flex-direction:column;gap:12px">
1235
+ <div class="gf-field" style="flex-direction:row;align-items:center;justify-content:space-between">
1236
+ <div style="display:flex;align-items:center;gap:4px">
1237
+ <label class="gf-label" style="margin:0">Provider</label>
1238
+ <span class="gf-help" id="gf-s-help" title="">?</span>
1239
+ </div>
1240
+ <div class="gf-picker" id="gf-s-provider-picker">
1241
+ <span class="gf-picker-value" id="gf-s-provider-label">${PROVIDERS[saved.provider]?.label || "OpenAI"}</span>
1242
+ </div>
1243
+ </div>
1244
+ <div class="gf-field">
1245
+ <label class="gf-label">API Key</label>
1246
+ <input type="password" class="gf-input gf-input-mono" id="gf-s-key" placeholder="sk-..." autocomplete="off" spellcheck="false" />
1247
+ </div>
1248
+ </div>
1249
+ <div class="gf-sep"></div>
1250
+ <div class="gf-field">
1251
+ <div style="display:flex;align-items:center;justify-content:space-between">
1252
+ <label class="gf-label" style="margin:0">Presets</label>
1253
+ <button class="gf-preset-chip add" id="gf-s-preset-add" style="font-size:10px;padding:2px 6px">+ Add</button>
1254
+ </div>
1255
+ <div class="gf-preset-list" id="gf-s-preset-list"></div>
1256
+ <div class="gf-preset-form" id="gf-s-preset-form" style="display:none">
1257
+ <input class="gf-input" id="gf-s-preset-name" placeholder="Name (e.g. D365)" />
1258
+ <textarea class="gf-input" id="gf-s-preset-prompt" placeholder="Prompt context..." rows="2" style="min-height:40px"></textarea>
1259
+ <div class="gf-preset-form-actions">
1260
+ <button class="gf-preset-form-btn cancel" id="gf-s-preset-cancel">Cancel</button>
1261
+ <button class="gf-preset-form-btn save" id="gf-s-preset-save">Save</button>
1262
+ </div>
1263
+ </div>
1264
+ </div>
1265
+ <button class="gf-save-btn" id="gf-s-save">Save</button>
1266
+ </div>
1267
+ `;
1268
+ shadow.appendChild(settingsPop);
1269
+ const sKeyInput = settingsPop.querySelector("#gf-s-key");
1270
+ const sUseAIToggle = settingsPop.querySelector("#gf-s-useai");
1271
+ const sAISection = settingsPop.querySelector("#gf-s-ai-section");
1272
+ const sHelpEl = settingsPop.querySelector("#gf-s-help");
1273
+ const sSaveBtn = settingsPop.querySelector("#gf-s-save");
1274
+ const sThemeBtn = settingsPop.querySelector("#gf-s-theme");
1275
+ const sColorsDiv = settingsPop.querySelector("#gf-s-colors");
1276
+ const sPickerEl = settingsPop.querySelector("#gf-s-provider-picker");
1277
+ const sPickerLabel = settingsPop.querySelector("#gf-s-provider-label");
1278
+ sKeyInput.value = saved.apiKey;
1279
+ const providerOrder = ["openai", "xai", "moonshot"];
1280
+ let selectedProvider = saved.provider || "openai";
1281
+ function updateProviderDisplay() {
1282
+ const p = PROVIDERS[selectedProvider] || PROVIDERS.openai;
1283
+ sPickerLabel.textContent = `${p.label} (${p.model})`;
1284
+ sHelpEl.title = p.helpText;
1285
+ }
1286
+ updateProviderDisplay();
1287
+ sPickerEl.addEventListener("click", () => {
1288
+ const idx = providerOrder.indexOf(selectedProvider);
1289
+ selectedProvider = providerOrder[(idx + 1) % providerOrder.length];
1290
+ updateProviderDisplay();
1291
+ });
1292
+ sUseAIToggle.addEventListener("change", () => {
1293
+ sAISection.style.display = sUseAIToggle.checked ? "flex" : "none";
1294
+ });
1295
+ let currentHighlightColor = saved.highlightColor || "#6366f1";
1296
+ sColorsDiv.addEventListener("click", (e) => {
1297
+ const dot = e.target.closest(".gf-color-dot");
1298
+ if (!dot) return;
1299
+ sColorsDiv.querySelectorAll(".gf-color-dot").forEach((d) => d.classList.remove("selected"));
1300
+ dot.classList.add("selected");
1301
+ currentHighlightColor = dot.dataset.color || "#6366f1";
1302
+ });
1303
+ let currentTheme = saved.theme || "dark";
1304
+ function applyTheme(theme) {
1305
+ currentTheme = theme;
1306
+ const isDark = theme === "dark";
1307
+ sThemeBtn.innerHTML = isDark ? ICONS.sun : ICONS.moon;
1308
+ const bg = isDark ? "#18181b" : "#ffffff";
1309
+ const bgInput = isDark ? "#09090b" : "#f4f4f5";
1310
+ const border = isDark ? "#27272a" : "#e4e4e7";
1311
+ const text = isDark ? "#fafafa" : "#18181b";
1312
+ const textMuted = isDark ? "#a1a1aa" : "#52525b";
1313
+ const textDim = isDark ? "#52525b" : "#a1a1aa";
1314
+ const btnHoverBg = isDark ? "#27272a" : "#f4f4f5";
1315
+ for (const pop of [settingsPop, promptPop]) {
1316
+ pop.style.background = bg;
1317
+ pop.style.boxShadow = isDark ? "0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06)" : "0 12px 40px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.08)";
1318
+ pop.querySelectorAll(".gf-pop-header h3").forEach((el) => el.style.color = text);
1319
+ pop.querySelectorAll(".gf-label").forEach((el) => el.style.color = textMuted);
1320
+ pop.querySelectorAll(".gf-input").forEach((el) => {
1321
+ el.style.background = bgInput;
1322
+ el.style.borderColor = border;
1323
+ el.style.color = text;
1324
+ });
1325
+ pop.querySelectorAll(".gf-sep").forEach((el) => el.style.background = border);
1326
+ pop.querySelectorAll(".gf-version").forEach((el) => el.style.color = textDim);
1327
+ pop.querySelectorAll(".gf-field-count").forEach((el) => el.style.color = textMuted);
1328
+ pop.querySelectorAll(".gf-fc-label").forEach((el) => el.style.color = isDark ? "#d4d4d8" : "#3f3f46");
1329
+ pop.querySelectorAll(".gf-field-card").forEach((el) => {
1330
+ el.style.background = isDark ? "#27272a" : "#f4f4f5";
1331
+ el.style.borderColor = border;
1332
+ });
1333
+ }
1334
+ bar.style.background = bg;
1335
+ bar.style.boxShadow = isDark ? "0 8px 32px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06)" : "0 8px 32px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.08)";
1336
+ bar.querySelectorAll(".gf-bar-btn").forEach((btn) => {
1337
+ btn.style.color = textMuted;
1338
+ btn.onmouseenter = () => {
1339
+ btn.style.background = btnHoverBg;
1340
+ btn.style.color = text;
1341
+ };
1342
+ btn.onmouseleave = () => {
1343
+ if (!btn.classList.contains("active")) {
1344
+ btn.style.background = "transparent";
1345
+ btn.style.color = textMuted;
1346
+ }
1347
+ };
1348
+ });
1349
+ bar.querySelectorAll(".gf-divider").forEach((el) => el.style.background = border);
1350
+ fab.style.background = bg;
1351
+ fab.style.color = textMuted;
1352
+ fab.style.boxShadow = isDark ? "0 4px 16px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06)" : "0 4px 16px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.08)";
1353
+ }
1354
+ if (currentTheme === "light") applyTheme("light");
1355
+ sThemeBtn.addEventListener("click", () => {
1356
+ applyTheme(currentTheme === "dark" ? "light" : "dark");
1357
+ });
1358
+ const promptPop = document.createElement("div");
1359
+ promptPop.className = "gf-popover";
1360
+ promptPop.style.width = "300px";
1361
+ promptPop.innerHTML = `
1362
+ <div class="gf-pop-header">
1363
+ <h3>Fill Fields</h3>
1364
+ <span class="gf-field-count" id="gf-p-count">0 fields</span>
1365
+ </div>
1366
+ <div class="gf-pop-body">
1367
+ <div id="gf-p-fields-wrap" style="display:none">
1368
+ <div class="gf-fields-grid" id="gf-p-grid"></div>
1369
+ </div>
1370
+ <div class="gf-field" id="gf-p-preset-row" style="display:none;flex-direction:row;align-items:center;justify-content:space-between">
1371
+ <label class="gf-label" style="margin:0">Preset</label>
1372
+ <div class="gf-picker" id="gf-p-preset-picker">
1373
+ <span class="gf-picker-value" id="gf-p-preset-label">None</span>
1374
+ </div>
1375
+ </div>
1376
+ <div class="gf-field" id="gf-p-prompt-wrap">
1377
+ <textarea class="gf-input" id="gf-p-prompt" placeholder="Optional: describe the data you want&#10;Leave empty for auto-generated data" rows="2"></textarea>
1378
+ </div>
1379
+ <button class="gf-fill-btn" id="gf-p-fill">
1380
+ ${ICONS.sparkles} Fill
1381
+ </button>
1382
+ <div class="gf-status info" id="gf-p-status"></div>
1383
+ </div>
1384
+ `;
1385
+ shadow.appendChild(promptPop);
1386
+ const pCountEl = promptPop.querySelector("#gf-p-count");
1387
+ const pFieldsWrap = promptPop.querySelector("#gf-p-fields-wrap");
1388
+ const pFieldGrid = promptPop.querySelector("#gf-p-grid");
1389
+ const pPresetRow = promptPop.querySelector("#gf-p-preset-row");
1390
+ const pPresetPicker = promptPop.querySelector("#gf-p-preset-picker");
1391
+ const pPresetLabel = promptPop.querySelector("#gf-p-preset-label");
1392
+ const pPromptWrap = promptPop.querySelector("#gf-p-prompt-wrap");
1393
+ const pPromptEl = promptPop.querySelector("#gf-p-prompt");
1394
+ const pFillBtn = promptPop.querySelector("#gf-p-fill");
1395
+ const pStatusEl = promptPop.querySelector("#gf-p-status");
1396
+ let presets = saved.presets || [];
1397
+ let activePresetId = saved.activePresetId || null;
1398
+ function updateFillPresetUI() {
1399
+ if (presets.length === 0) {
1400
+ pPresetRow.style.display = "none";
1401
+ pPromptWrap.style.display = "flex";
1402
+ activePresetId = null;
1403
+ return;
1404
+ }
1405
+ pPresetRow.style.display = "flex";
1406
+ const active = activePresetId ? presets.find((p) => p.id === activePresetId) : null;
1407
+ pPresetLabel.textContent = active ? active.name : "None";
1408
+ pPromptWrap.style.display = active ? "none" : "flex";
1409
+ }
1410
+ function persistActivePreset() {
1411
+ const s = loadSettings();
1412
+ s.activePresetId = activePresetId;
1413
+ saveSettings(s);
1414
+ }
1415
+ pPresetPicker.addEventListener("click", () => {
1416
+ if (presets.length === 0) return;
1417
+ if (!activePresetId) {
1418
+ activePresetId = presets[0].id;
1419
+ } else {
1420
+ const idx = presets.findIndex((p) => p.id === activePresetId);
1421
+ if (idx < presets.length - 1) {
1422
+ activePresetId = presets[idx + 1].id;
1423
+ } else {
1424
+ activePresetId = null;
1425
+ }
1426
+ }
1427
+ persistActivePreset();
1428
+ updateFillPresetUI();
1429
+ });
1430
+ updateFillPresetUI();
1431
+ function setStatus(text, type) {
1432
+ pStatusEl.textContent = text;
1433
+ pStatusEl.className = `gf-status ${type}`;
1434
+ }
1435
+ function clearStatus() {
1436
+ pStatusEl.textContent = "";
1437
+ pStatusEl.className = "gf-status info";
1438
+ }
1439
+ function repositionPopover() {
1440
+ const barRect = bar.getBoundingClientRect();
1441
+ const popCenterX = barRect.left + barRect.width / 2;
1442
+ const popBottom = barRect.top - 8;
1443
+ for (const pop of [settingsPop, promptPop]) {
1444
+ pop.style.position = "fixed";
1445
+ pop.style.bottom = "";
1446
+ pop.style.left = `${popCenterX}px`;
1447
+ pop.style.top = `${popBottom}px`;
1448
+ pop.style.transform = "translate(-50%, -100%)";
1449
+ }
1450
+ }
1451
+ let currentPopover = null;
1452
+ function openPopover(name) {
1453
+ settingsPop.classList.remove("open");
1454
+ promptPop.classList.remove("open");
1455
+ btnSettings.classList.remove("active");
1456
+ btnFill.classList.remove("active");
1457
+ if (name === currentPopover || name === null) {
1458
+ currentPopover = null;
1459
+ return;
1460
+ }
1461
+ repositionPopover();
1462
+ currentPopover = name;
1463
+ if (name === "settings") {
1464
+ settingsPop.classList.add("open");
1465
+ btnSettings.classList.add("active");
1466
+ sKeyInput.focus();
1467
+ } else if (name === "prompt") {
1468
+ promptPop.classList.add("open");
1469
+ btnFill.classList.add("active");
1470
+ pPromptEl.focus();
1471
+ }
1472
+ }
1473
+ let blockHighlight = null;
1474
+ function highlightBlock(el) {
1475
+ removeBlockHighlight();
1476
+ blockHighlight = document.createElement("div");
1477
+ blockHighlight.id = "ghostfill-block-highlight";
1478
+ const rect = el.getBoundingClientRect();
1479
+ const c = currentHighlightColor;
1480
+ Object.assign(blockHighlight.style, {
1481
+ position: "fixed",
1482
+ top: `${rect.top}px`,
1483
+ left: `${rect.left}px`,
1484
+ width: `${rect.width}px`,
1485
+ height: `${rect.height}px`,
1486
+ border: `2px solid ${c}`,
1487
+ borderRadius: "6px",
1488
+ backgroundColor: `${c}0d`,
1489
+ pointerEvents: "none",
1490
+ zIndex: "2147483644",
1491
+ transition: "all 0.2s"
1492
+ });
1493
+ document.body.appendChild(blockHighlight);
1494
+ }
1495
+ function removeBlockHighlight() {
1496
+ blockHighlight?.remove();
1497
+ blockHighlight = null;
1498
+ }
1499
+ function fieldTypeIcon(type) {
1500
+ switch (type) {
1501
+ case "email":
1502
+ return ICONS.ftEmail;
1503
+ case "tel":
1504
+ return ICONS.ftPhone;
1505
+ case "number":
1506
+ case "range":
1507
+ return ICONS.ftNumber;
1508
+ case "date":
1509
+ case "datetime-local":
1510
+ case "time":
1511
+ case "month":
1512
+ case "week":
1513
+ return ICONS.ftDate;
1514
+ case "select":
1515
+ return ICONS.ftSelect;
1516
+ case "textarea":
1517
+ return ICONS.ftTextarea;
1518
+ case "checkbox":
1519
+ return ICONS.ftCheckbox;
1520
+ case "radio":
1521
+ return ICONS.ftRadio;
1522
+ case "url":
1523
+ return ICONS.ftUrl;
1524
+ case "password":
1525
+ return ICONS.ftPassword;
1526
+ case "file":
1527
+ return ICONS.ftFile;
1528
+ default:
1529
+ return ICONS.ftText;
1530
+ }
1531
+ }
1532
+ let fieldHighlightEl = null;
1533
+ function highlightField(el) {
1534
+ clearFieldHighlight();
1535
+ el.style.outline = `2px solid ${currentHighlightColor}`;
1536
+ el.style.outlineOffset = "2px";
1537
+ el.style.transition = "outline 0.15s, outline-offset 0.15s";
1538
+ fieldHighlightEl = el;
1539
+ }
1540
+ function clearFieldHighlight() {
1541
+ if (fieldHighlightEl) {
1542
+ fieldHighlightEl.style.outline = "";
1543
+ fieldHighlightEl.style.outlineOffset = "";
1544
+ fieldHighlightEl = null;
1545
+ }
1546
+ }
1547
+ function showFieldsInPrompt(fields) {
1548
+ pCountEl.textContent = `${fields.length} field${fields.length === 1 ? "" : "s"}`;
1549
+ pFieldGrid.innerHTML = "";
1550
+ fields.forEach((f) => {
1551
+ const card = document.createElement("div");
1552
+ card.className = "gf-field-card";
1553
+ const icon = document.createElement("span");
1554
+ icon.className = "gf-fc-icon";
1555
+ icon.innerHTML = fieldTypeIcon(f.type);
1556
+ const label = document.createElement("span");
1557
+ label.className = "gf-fc-label";
1558
+ label.textContent = f.label;
1559
+ card.appendChild(icon);
1560
+ card.appendChild(label);
1561
+ if (f.required) {
1562
+ const req = document.createElement("span");
1563
+ req.className = "gf-fc-req";
1564
+ req.textContent = "*";
1565
+ card.appendChild(req);
1566
+ }
1567
+ card.addEventListener("mouseenter", () => highlightField(f.element));
1568
+ card.addEventListener("mouseleave", () => clearFieldHighlight());
1569
+ pFieldGrid.appendChild(card);
1570
+ });
1571
+ pFieldsWrap.style.display = "block";
1572
+ }
1573
+ let cleanupSelector = null;
1574
+ btnSelect.addEventListener("click", () => {
1575
+ if (hasDragged) return;
1576
+ if (state.selecting) {
1577
+ cleanupSelector?.();
1578
+ cleanupSelector = null;
1579
+ state.selecting = false;
1580
+ btnSelect.classList.remove("active");
1581
+ return;
1582
+ }
1583
+ openPopover(null);
1584
+ state.selecting = true;
1585
+ btnSelect.classList.add("active");
1586
+ cleanupSelector = startSelection(
1587
+ (element) => {
1588
+ state.selecting = false;
1589
+ state.selectedBlock = element;
1590
+ btnSelect.classList.remove("active");
1591
+ cleanupSelector = null;
1592
+ const fields = detectFields(element);
1593
+ state.fields = fields;
1594
+ if (fields.length === 0) {
1595
+ badge.style.display = "none";
1596
+ btnFill.disabled = true;
1597
+ return;
1598
+ }
1599
+ highlightBlock(element);
1600
+ showFieldsInPrompt(fields);
1601
+ badge.textContent = String(fields.length);
1602
+ badge.style.display = "flex";
1603
+ btnFill.disabled = false;
1604
+ },
1605
+ () => {
1606
+ state.selecting = false;
1607
+ btnSelect.classList.remove("active");
1608
+ cleanupSelector = null;
1609
+ },
1610
+ host,
1611
+ currentHighlightColor
1612
+ );
1613
+ });
1614
+ btnFill.addEventListener("click", () => {
1615
+ if (hasDragged) return;
1616
+ if (state.fields.length === 0) return;
1617
+ openPopover("prompt");
1618
+ });
1619
+ btnSettings.addEventListener("click", () => {
1620
+ if (hasDragged) return;
1621
+ openPopover("settings");
1622
+ });
1623
+ btnMinimize.addEventListener("click", () => {
1624
+ if (hasDragged) return;
1625
+ openPopover(null);
1626
+ removeBlockHighlight();
1627
+ cleanupSelector?.();
1628
+ state.selecting = false;
1629
+ state.selectedBlock = null;
1630
+ state.fields = [];
1631
+ badge.style.display = "none";
1632
+ btnFill.disabled = true;
1633
+ positionFab();
1634
+ bar.style.display = "none";
1635
+ fab.classList.add("visible");
1636
+ state.active = false;
1637
+ });
1638
+ let fabDragState = { dragging: false, moved: false, startX: 0, startY: 0, fabX: 0, fabY: 0 };
1639
+ function onFabMouseDown(e) {
1640
+ fabDragState = {
1641
+ dragging: true,
1642
+ moved: false,
1643
+ startX: e.clientX,
1644
+ startY: e.clientY,
1645
+ fabX: fab.getBoundingClientRect().left,
1646
+ fabY: fab.getBoundingClientRect().top
1647
+ };
1648
+ const onMove = (ev) => {
1649
+ const dx = ev.clientX - fabDragState.startX;
1650
+ const dy = ev.clientY - fabDragState.startY;
1651
+ if (!fabDragState.moved && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) fabDragState.moved = true;
1652
+ if (!fabDragState.moved) return;
1653
+ fab.style.left = `${Math.max(0, Math.min(window.innerWidth - 44, fabDragState.fabX + dx))}px`;
1654
+ fab.style.top = `${Math.max(0, Math.min(window.innerHeight - 44, fabDragState.fabY + dy))}px`;
1655
+ fab.style.right = "";
1656
+ fab.style.bottom = "";
1657
+ };
1658
+ const onUp = () => {
1659
+ document.removeEventListener("mousemove", onMove);
1660
+ document.removeEventListener("mouseup", onUp);
1661
+ fabDragState.dragging = false;
1662
+ if (fabDragState.moved) {
1663
+ localStorage.setItem(FAB_POS_KEY, JSON.stringify({ x: fab.getBoundingClientRect().left, y: fab.getBoundingClientRect().top }));
1664
+ }
1665
+ };
1666
+ document.addEventListener("mousemove", onMove);
1667
+ document.addEventListener("mouseup", onUp);
1668
+ }
1669
+ fab.addEventListener("mousedown", onFabMouseDown);
1670
+ fab.addEventListener("click", () => {
1671
+ if (fabDragState.moved) {
1672
+ fabDragState.moved = false;
1673
+ return;
1674
+ }
1675
+ fab.classList.remove("visible");
1676
+ bar.style.left = fab.style.left || `${window.innerWidth - 250}px`;
1677
+ bar.style.top = fab.style.top || `${window.innerHeight - 80}px`;
1678
+ bar.style.transform = "none";
1679
+ bar.style.bottom = "";
1680
+ bar.style.display = "flex";
1681
+ state.active = true;
1682
+ setTimeout(() => btnSelect.click(), 50);
1683
+ });
1684
+ const sPresetList = settingsPop.querySelector("#gf-s-preset-list");
1685
+ const sPresetForm = settingsPop.querySelector("#gf-s-preset-form");
1686
+ const sPresetAddBtn = settingsPop.querySelector("#gf-s-preset-add");
1687
+ const sPresetName = settingsPop.querySelector("#gf-s-preset-name");
1688
+ const sPresetPrompt = settingsPop.querySelector("#gf-s-preset-prompt");
1689
+ const sPresetSaveBtn = settingsPop.querySelector("#gf-s-preset-save");
1690
+ const sPresetCancelBtn = settingsPop.querySelector("#gf-s-preset-cancel");
1691
+ let editingPresetId = null;
1692
+ function renderPresetList() {
1693
+ sPresetList.innerHTML = "";
1694
+ presets.forEach((p) => {
1695
+ const item = document.createElement("div");
1696
+ item.className = "gf-preset-item";
1697
+ const name = document.createElement("span");
1698
+ name.className = "gf-preset-item-name";
1699
+ name.textContent = p.name;
1700
+ name.style.cursor = "pointer";
1701
+ name.addEventListener("click", () => {
1702
+ editingPresetId = p.id;
1703
+ sPresetForm.style.display = "flex";
1704
+ sPresetName.value = p.name;
1705
+ sPresetPrompt.value = p.prompt;
1706
+ sPresetName.focus();
1707
+ });
1708
+ const del = document.createElement("button");
1709
+ del.className = "gf-preset-del";
1710
+ del.innerHTML = "&times;";
1711
+ del.addEventListener("click", () => {
1712
+ presets = presets.filter((x) => x.id !== p.id);
1713
+ if (activePresetId === p.id) activePresetId = null;
1714
+ renderPresetList();
1715
+ updateFillPresetUI();
1716
+ });
1717
+ item.append(name, del);
1718
+ sPresetList.appendChild(item);
1719
+ });
1720
+ }
1721
+ renderPresetList();
1722
+ sPresetAddBtn.addEventListener("click", () => {
1723
+ editingPresetId = null;
1724
+ sPresetForm.style.display = "flex";
1725
+ sPresetName.value = "";
1726
+ sPresetPrompt.value = "";
1727
+ sPresetName.focus();
1728
+ });
1729
+ sPresetCancelBtn.addEventListener("click", () => {
1730
+ sPresetForm.style.display = "none";
1731
+ editingPresetId = null;
1732
+ });
1733
+ sPresetSaveBtn.addEventListener("click", () => {
1734
+ const name = sPresetName.value.trim();
1735
+ const prompt = sPresetPrompt.value.trim();
1736
+ if (!name || !prompt) return;
1737
+ if (editingPresetId) {
1738
+ const idx = presets.findIndex((p) => p.id === editingPresetId);
1739
+ if (idx >= 0) presets[idx] = { ...presets[idx], name, prompt };
1740
+ } else {
1741
+ presets.push({ id: Date.now().toString(36), name, prompt });
1742
+ }
1743
+ editingPresetId = null;
1744
+ sPresetForm.style.display = "none";
1745
+ renderPresetList();
1746
+ updateFillPresetUI();
1747
+ });
1748
+ sSaveBtn.addEventListener("click", () => {
1749
+ const s = {
1750
+ apiKey: sKeyInput.value.trim(),
1751
+ provider: selectedProvider,
1752
+ highlightColor: currentHighlightColor,
1753
+ theme: currentTheme,
1754
+ useAI: sUseAIToggle.checked,
1755
+ presets,
1756
+ activePresetId
1757
+ };
1758
+ saveSettings(s);
1759
+ dotWarn.style.display = s.useAI && !s.apiKey ? "block" : "none";
1760
+ openPopover(null);
1761
+ });
1762
+ async function doFill() {
1763
+ const settings = loadSettings();
1764
+ const activePreset = activePresetId ? (settings.presets || []).find((p) => p.id === activePresetId) : null;
1765
+ const userText = pPromptEl.value.trim();
1766
+ const promptText = [activePreset?.prompt, userText].filter(Boolean).join("\n\n");
1767
+ if (state.fields.length === 0) return;
1768
+ pFillBtn.disabled = true;
1769
+ pFillBtn.innerHTML = `<span class="gf-spin">${ICONS.spinner}</span> Filling...`;
1770
+ clearStatus();
1771
+ try {
1772
+ let fillData;
1773
+ if (settings.useAI) {
1774
+ if (!settings.apiKey) {
1775
+ setStatus("Set your API key in Settings first", "error");
1776
+ pFillBtn.disabled = false;
1777
+ pFillBtn.innerHTML = `${ICONS.sparkles} Fill`;
1778
+ return;
1779
+ }
1780
+ const blockContext = state.selectedBlock ? extractBlockContext(state.selectedBlock) : "";
1781
+ fillData = await generateFillData(
1782
+ state.fields,
1783
+ promptText,
1784
+ settings,
1785
+ options.systemPrompt,
1786
+ blockContext
1787
+ );
1788
+ } else {
1789
+ fillData = generateFakeData(state.fields);
1790
+ }
1791
+ const { filled, errors } = await fillFields(state.fields, fillData);
1792
+ if (errors.length > 0) {
1793
+ setStatus(`Filled ${filled}/${state.fields.length} fields`, filled > 0 ? "success" : "error");
1794
+ } else {
1795
+ setStatus(`Filled ${filled} field${filled === 1 ? "" : "s"}`, "success");
1796
+ }
1797
+ removeBlockHighlight();
1798
+ setTimeout(() => openPopover(null), 600);
1799
+ } catch (err) {
1800
+ setStatus(cleanError(err), "error");
1801
+ } finally {
1802
+ pFillBtn.disabled = false;
1803
+ pFillBtn.innerHTML = `${ICONS.sparkles} Fill`;
1804
+ }
1805
+ }
1806
+ pFillBtn.addEventListener("click", doFill);
1807
+ pPromptEl.addEventListener("keydown", (e) => {
1808
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
1809
+ e.preventDefault();
1810
+ doFill();
1811
+ }
1812
+ });
1813
+ const shortcut = options.shortcut || "Alt+G";
1814
+ const keys = shortcut.toLowerCase().split("+");
1815
+ function handleShortcut(e) {
1816
+ const mainKey = keys.filter(
1817
+ (k) => !["alt", "ctrl", "control", "shift", "meta", "cmd"].includes(k)
1818
+ )[0];
1819
+ const match = (keys.includes("alt") ? e.altKey : !e.altKey) && (keys.includes("ctrl") || keys.includes("control") ? e.ctrlKey : !e.ctrlKey) && (keys.includes("shift") ? e.shiftKey : !e.shiftKey) && (keys.includes("meta") || keys.includes("cmd") ? e.metaKey : !e.metaKey) && e.key.toLowerCase() === mainKey;
1820
+ if (match) {
1821
+ e.preventDefault();
1822
+ if (state.active) {
1823
+ openPopover(null);
1824
+ positionFab();
1825
+ bar.style.display = "none";
1826
+ fab.classList.add("visible");
1827
+ state.active = false;
1828
+ } else {
1829
+ fab.classList.remove("visible");
1830
+ bar.style.display = "flex";
1831
+ state.active = true;
1832
+ }
1833
+ }
1834
+ }
1835
+ document.addEventListener("keydown", handleShortcut);
1836
+ function destroy() {
1837
+ cleanupSelector?.();
1838
+ removeBlockHighlight();
1839
+ document.removeEventListener("keydown", handleShortcut);
1840
+ host.remove();
1841
+ }
1842
+ return { state, destroy };
1843
+ }
1844
+
1845
+ // src/index.ts
1846
+ var instance = null;
1847
+ function init(options = {}) {
1848
+ const existing = document.getElementById("ghostfill-root");
1849
+ if (existing && instance) {
1850
+ return instance;
1851
+ }
1852
+ if (instance) {
1853
+ instance.destroy();
1854
+ instance = null;
1855
+ }
1856
+ const { state, destroy } = createOverlay2(options);
1857
+ instance = { destroy };
1858
+ return { destroy };
1859
+ }
1860
+ async function fill(params) {
1861
+ const fields = detectFields(params.container);
1862
+ if (fields.length === 0) {
1863
+ return { filled: 0, errors: ["No fillable fields found in container"] };
1864
+ }
1865
+ const fillData = generateFakeData(fields);
1866
+ return fillFields(fields, fillData);
1867
+ }
1868
+ // Annotate the CommonJS export names for ESM import in node:
1869
+ 0 && (module.exports = {
1870
+ fill,
1871
+ init
1872
+ });
1873
+ //# sourceMappingURL=index.js.map