vibespot 0.4.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/LICENSE +33 -0
- package/README.md +118 -0
- package/assets/content-guide.md +445 -0
- package/assets/conversion-guide.md +693 -0
- package/assets/design-guide.md +380 -0
- package/assets/hubspot-rules.md +560 -0
- package/assets/page-types.md +116 -0
- package/bin/vibespot.mjs +11 -0
- package/dist/index.js +6552 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
- package/ui/chat.js +803 -0
- package/ui/dashboard.js +383 -0
- package/ui/dialog.js +117 -0
- package/ui/field-editor.js +292 -0
- package/ui/index.html +393 -0
- package/ui/preview.js +132 -0
- package/ui/settings.js +927 -0
- package/ui/setup.js +830 -0
- package/ui/styles.css +2552 -0
- package/ui/upload-panel.js +554 -0
package/ui/dashboard.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard screen — project overview with templates, module library, and brand assets.
|
|
3
|
+
* Sits between setup (project list) and chat (template editing).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const dashboardScreen = document.getElementById("dashboard-screen");
|
|
7
|
+
|
|
8
|
+
// Page type labels for display
|
|
9
|
+
const PAGE_TYPE_LABELS = {
|
|
10
|
+
landing_page: "LP",
|
|
11
|
+
blog_post: "Blog",
|
|
12
|
+
website_page: "Web",
|
|
13
|
+
module_only: "Mod",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const PAGE_TYPE_FULL_LABELS = {
|
|
17
|
+
landing_page: "Landing Page",
|
|
18
|
+
blog_post: "Blog Post",
|
|
19
|
+
website_page: "Website Page",
|
|
20
|
+
module_only: "Module Only",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Show / hide dashboard
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
let currentDashboardTheme = "";
|
|
28
|
+
|
|
29
|
+
async function showDashboard(themeName) {
|
|
30
|
+
currentDashboardTheme = themeName;
|
|
31
|
+
|
|
32
|
+
// Hide other screens
|
|
33
|
+
setupScreen.classList.add("hidden");
|
|
34
|
+
document.getElementById("setup-topbar").classList.add("hidden");
|
|
35
|
+
appScreen.classList.add("hidden");
|
|
36
|
+
dashboardScreen.classList.remove("hidden");
|
|
37
|
+
|
|
38
|
+
document.getElementById("dashboard-theme-name").textContent = themeName;
|
|
39
|
+
|
|
40
|
+
// Update URL
|
|
41
|
+
const target = "#/dashboard/" + encodeURIComponent(themeName);
|
|
42
|
+
if (location.hash !== target) {
|
|
43
|
+
history.pushState(null, "", target);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Load dashboard data
|
|
47
|
+
await refreshDashboard();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hideDashboard() {
|
|
51
|
+
dashboardScreen.classList.add("hidden");
|
|
52
|
+
currentDashboardTheme = "";
|
|
53
|
+
closeModulePreview();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Load dashboard data from server
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
async function refreshDashboard() {
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch("/api/dashboard");
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
if (data.error) {
|
|
65
|
+
console.warn("Dashboard load error:", data.error);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
renderTemplateList(data.templates || []);
|
|
69
|
+
renderModuleLibrary(data.moduleLibrary || []);
|
|
70
|
+
renderBrandAssets(data.brandAssets || {});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error("Failed to load dashboard:", err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Template list
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
function renderTemplateList(templates) {
|
|
81
|
+
const list = document.getElementById("dashboard-template-list");
|
|
82
|
+
const countEl = document.getElementById("dashboard-template-count");
|
|
83
|
+
countEl.textContent = templates.length;
|
|
84
|
+
|
|
85
|
+
if (templates.length === 0) {
|
|
86
|
+
list.innerHTML = `<p class="dashboard__empty-state">No templates yet. Choose a page type above to get started.</p>`;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
list.innerHTML = "";
|
|
91
|
+
for (const tpl of templates) {
|
|
92
|
+
const item = document.createElement("div");
|
|
93
|
+
item.className = "dashboard__template-item";
|
|
94
|
+
item.innerHTML = `
|
|
95
|
+
<span class="dashboard__template-badge dashboard__template-badge--${tpl.pageType}">${esc(PAGE_TYPE_LABELS[tpl.pageType] || "?")}</span>
|
|
96
|
+
<span class="dashboard__template-label">${esc(tpl.label)}</span>
|
|
97
|
+
<span class="dashboard__template-meta">${tpl.moduleCount} module${tpl.moduleCount !== 1 ? "s" : ""}</span>
|
|
98
|
+
<button class="btn btn--sm btn--primary dashboard__template-open" data-id="${esc(tpl.id)}">Open</button>
|
|
99
|
+
<button class="dashboard__template-delete" data-id="${esc(tpl.id)}" title="Delete template">×</button>
|
|
100
|
+
`;
|
|
101
|
+
list.appendChild(item);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Attach click handlers
|
|
105
|
+
list.querySelectorAll(".dashboard__template-open").forEach((btn) => {
|
|
106
|
+
btn.addEventListener("click", () => openTemplate(btn.dataset.id));
|
|
107
|
+
});
|
|
108
|
+
list.querySelectorAll(".dashboard__template-delete").forEach((btn) => {
|
|
109
|
+
btn.addEventListener("click", () => confirmDeleteTemplate(btn.dataset.id));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Module library
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
let activePreviewModule = "";
|
|
118
|
+
|
|
119
|
+
function renderModuleLibrary(modules) {
|
|
120
|
+
const container = document.getElementById("dashboard-module-library");
|
|
121
|
+
|
|
122
|
+
if (modules.length === 0) {
|
|
123
|
+
container.innerHTML = `<p class="dashboard__empty-state">Modules will appear here as you build pages.</p>`;
|
|
124
|
+
closeModulePreview();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
container.innerHTML = "";
|
|
129
|
+
for (const mod of modules) {
|
|
130
|
+
const chip = document.createElement("span");
|
|
131
|
+
chip.className = "dashboard__module-chip";
|
|
132
|
+
if (mod.moduleName === activePreviewModule) chip.classList.add("dashboard__module-chip--active");
|
|
133
|
+
chip.textContent = mod.moduleName;
|
|
134
|
+
chip.title = `Used in: ${mod.usedIn.join(", ")}`;
|
|
135
|
+
chip.dataset.module = mod.moduleName;
|
|
136
|
+
chip.dataset.usedIn = mod.usedIn.join(", ");
|
|
137
|
+
chip.addEventListener("click", () => toggleModulePreview(mod.moduleName, mod.usedIn));
|
|
138
|
+
container.appendChild(chip);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function toggleModulePreview(moduleName, usedIn) {
|
|
143
|
+
if (activePreviewModule === moduleName) {
|
|
144
|
+
closeModulePreview();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
showModulePreview(moduleName, usedIn);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function showModulePreview(moduleName, usedIn) {
|
|
151
|
+
const previewEl = document.getElementById("dashboard-module-preview");
|
|
152
|
+
const nameEl = document.getElementById("dashboard-preview-name");
|
|
153
|
+
const usedEl = document.getElementById("dashboard-preview-used");
|
|
154
|
+
const frame = document.getElementById("dashboard-preview-frame");
|
|
155
|
+
|
|
156
|
+
activePreviewModule = moduleName;
|
|
157
|
+
|
|
158
|
+
// Update active chip styling
|
|
159
|
+
document.querySelectorAll(".dashboard__module-chip").forEach((c) => {
|
|
160
|
+
c.classList.toggle("dashboard__module-chip--active", c.dataset.module === moduleName);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
nameEl.textContent = moduleName;
|
|
164
|
+
usedEl.textContent = usedIn ? `Used in: ${usedIn.join(", ")}` : "";
|
|
165
|
+
previewEl.classList.remove("hidden");
|
|
166
|
+
|
|
167
|
+
// Load preview into iframe
|
|
168
|
+
try {
|
|
169
|
+
const res = await fetch(`/module-preview?module=${encodeURIComponent(moduleName)}`);
|
|
170
|
+
const html = await res.text();
|
|
171
|
+
frame.srcdoc = html;
|
|
172
|
+
} catch {
|
|
173
|
+
frame.srcdoc = "<p style='padding:2rem;color:#888;font-family:sans-serif'>Preview unavailable</p>";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function closeModulePreview() {
|
|
178
|
+
activePreviewModule = "";
|
|
179
|
+
const previewEl = document.getElementById("dashboard-module-preview");
|
|
180
|
+
previewEl.classList.add("hidden");
|
|
181
|
+
document.querySelectorAll(".dashboard__module-chip").forEach((c) => {
|
|
182
|
+
c.classList.remove("dashboard__module-chip--active");
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Close button for module preview
|
|
187
|
+
document.getElementById("dashboard-preview-close").addEventListener("click", closeModulePreview);
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Brand assets
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
function renderBrandAssets(assets) {
|
|
194
|
+
const sgIcon = document.getElementById("brand-icon-styleguide");
|
|
195
|
+
const bvIcon = document.getElementById("brand-icon-brandvoice");
|
|
196
|
+
|
|
197
|
+
if (assets.hasStyleguide) {
|
|
198
|
+
sgIcon.textContent = "\u2713";
|
|
199
|
+
sgIcon.classList.add("brand-asset-upload__icon--done");
|
|
200
|
+
} else {
|
|
201
|
+
sgIcon.textContent = "+";
|
|
202
|
+
sgIcon.classList.remove("brand-asset-upload__icon--done");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (assets.hasBrandvoice) {
|
|
206
|
+
bvIcon.textContent = "\u2713";
|
|
207
|
+
bvIcon.classList.add("brand-asset-upload__icon--done");
|
|
208
|
+
} else {
|
|
209
|
+
bvIcon.textContent = "+";
|
|
210
|
+
bvIcon.classList.remove("brand-asset-upload__icon--done");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Actions
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
async function createTemplateFromPageType(pageType) {
|
|
219
|
+
const defaultLabels = {
|
|
220
|
+
landing_page: "Landing Page",
|
|
221
|
+
blog_post: "Blog Post",
|
|
222
|
+
website_page: "Website Page",
|
|
223
|
+
module_only: "Module",
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const label = await vibePrompt("Template name", defaultLabels[pageType] || "New Template");
|
|
227
|
+
if (!label) return;
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const res = await fetch("/api/templates", {
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers: { "Content-Type": "application/json" },
|
|
233
|
+
body: JSON.stringify({ pageType, label }),
|
|
234
|
+
});
|
|
235
|
+
const data = await res.json();
|
|
236
|
+
if (data.error) {
|
|
237
|
+
await vibeAlert(data.error, "Error");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Open the newly created template in chat
|
|
242
|
+
openTemplate(data.template.id);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
await vibeAlert("Failed to create template: " + err.message, "Error");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function openTemplate(templateId) {
|
|
249
|
+
try {
|
|
250
|
+
// Activate the template on the server
|
|
251
|
+
const res = await fetch("/api/templates/activate", {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: { "Content-Type": "application/json" },
|
|
254
|
+
body: JSON.stringify({ templateId }),
|
|
255
|
+
});
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
if (data.error) {
|
|
258
|
+
await vibeAlert(data.error, "Error");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Transition to chat screen
|
|
263
|
+
showChat(currentDashboardTheme, templateId);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
await vibeAlert("Failed to open template: " + err.message, "Error");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function confirmDeleteTemplate(templateId) {
|
|
270
|
+
const ok = await vibeConfirm("Delete this template?", "This cannot be undone.", { confirmLabel: "Delete" });
|
|
271
|
+
if (!ok) return;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
await fetch("/api/templates", {
|
|
275
|
+
method: "DELETE",
|
|
276
|
+
headers: { "Content-Type": "application/json" },
|
|
277
|
+
body: JSON.stringify({ templateId }),
|
|
278
|
+
});
|
|
279
|
+
await refreshDashboard();
|
|
280
|
+
} catch (err) {
|
|
281
|
+
await vibeAlert("Failed to delete: " + err.message, "Error");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function uploadBrandAsset(type) {
|
|
286
|
+
const uploadEl = document.getElementById(`brand-upload-${type}`);
|
|
287
|
+
const fileInput = uploadEl.querySelector("input[type=file]");
|
|
288
|
+
|
|
289
|
+
// Trigger file picker
|
|
290
|
+
fileInput.click();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function handleBrandFileSelected(type, file) {
|
|
294
|
+
const content = await file.text();
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const res = await fetch("/api/brand-assets", {
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers: { "Content-Type": "application/json" },
|
|
300
|
+
body: JSON.stringify({ type, content }),
|
|
301
|
+
});
|
|
302
|
+
const data = await res.json();
|
|
303
|
+
if (data.error) {
|
|
304
|
+
await vibeAlert(data.error, "Error");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
refreshDashboard();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
await vibeAlert("Failed to upload: " + err.message, "Error");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Chat screen transition
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
function showChat(themeName, templateId) {
|
|
318
|
+
hideDashboard();
|
|
319
|
+
|
|
320
|
+
// Show app screen
|
|
321
|
+
appScreen.classList.remove("hidden");
|
|
322
|
+
document.getElementById("theme-name").textContent = themeName;
|
|
323
|
+
|
|
324
|
+
// Update browser chrome URL bar
|
|
325
|
+
const urlEl = document.getElementById("browser-url");
|
|
326
|
+
if (urlEl) urlEl.textContent = themeName + ".vibespot.app";
|
|
327
|
+
|
|
328
|
+
// Update URL
|
|
329
|
+
const target = `#/app/${encodeURIComponent(themeName)}/${encodeURIComponent(templateId)}`;
|
|
330
|
+
if (location.hash !== target) {
|
|
331
|
+
history.pushState(null, "", target);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Connect WebSocket (defined in chat.js)
|
|
335
|
+
if (typeof connectWebSocket === "function") {
|
|
336
|
+
connectWebSocket();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Load initial preview
|
|
340
|
+
if (typeof refreshPreview === "function") {
|
|
341
|
+
refreshPreview();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Event listeners
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
// Page type cards
|
|
350
|
+
document.querySelectorAll(".page-type-card").forEach((card) => {
|
|
351
|
+
card.addEventListener("click", () => {
|
|
352
|
+
createTemplateFromPageType(card.dataset.type);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Back button → setup
|
|
357
|
+
document.getElementById("dashboard-back").addEventListener("click", () => {
|
|
358
|
+
hideDashboard();
|
|
359
|
+
if (typeof showSetup === "function") showSetup();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Settings button
|
|
363
|
+
document.getElementById("dashboard-settings-btn").addEventListener("click", () => {
|
|
364
|
+
if (typeof openSettings === "function") openSettings();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Deploy button
|
|
368
|
+
document.getElementById("dashboard-deploy-btn").addEventListener("click", () => {
|
|
369
|
+
if (typeof startUpload === "function") {
|
|
370
|
+
// Need to show app screen temporarily for upload
|
|
371
|
+
appScreen.classList.remove("hidden");
|
|
372
|
+
dashboardScreen.classList.add("hidden");
|
|
373
|
+
startUpload();
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Brand asset file inputs
|
|
378
|
+
document.getElementById("brand-upload-styleguide").querySelector("input").addEventListener("change", (e) => {
|
|
379
|
+
if (e.target.files[0]) handleBrandFileSelected("styleguide", e.target.files[0]);
|
|
380
|
+
});
|
|
381
|
+
document.getElementById("brand-upload-brandvoice").querySelector("input").addEventListener("change", (e) => {
|
|
382
|
+
if (e.target.files[0]) handleBrandFileSelected("brandvoice", e.target.files[0]);
|
|
383
|
+
});
|
package/ui/dialog.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared modal dialog utilities — styled replacements for native alert/confirm/prompt.
|
|
3
|
+
* Uses the existing .confirm-overlay / .confirm-dialog CSS pattern.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// HTML-escape helper (standalone so dialog.js has no load-order dependency)
|
|
7
|
+
if (typeof esc === "undefined") {
|
|
8
|
+
// eslint-disable-next-line no-var
|
|
9
|
+
var esc = function (str) {
|
|
10
|
+
const el = document.createElement("span");
|
|
11
|
+
el.textContent = String(str);
|
|
12
|
+
return el.innerHTML;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Show a styled alert dialog (replaces window.alert).
|
|
18
|
+
* @param {string} message — the message to display
|
|
19
|
+
* @param {string} [title] — optional dialog title
|
|
20
|
+
* @returns {Promise<void>}
|
|
21
|
+
*/
|
|
22
|
+
function vibeAlert(message, title) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const overlay = document.createElement("div");
|
|
25
|
+
overlay.className = "confirm-overlay";
|
|
26
|
+
overlay.innerHTML = `
|
|
27
|
+
<div class="confirm-dialog">
|
|
28
|
+
${title ? `<div class="confirm-dialog__title">${esc(title)}</div>` : ""}
|
|
29
|
+
<p class="confirm-dialog__detail">${esc(message)}</p>
|
|
30
|
+
<div class="confirm-dialog__actions">
|
|
31
|
+
<button class="btn btn--primary" data-action="ok">OK</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
`;
|
|
35
|
+
document.body.appendChild(overlay);
|
|
36
|
+
|
|
37
|
+
const close = () => { overlay.remove(); resolve(); };
|
|
38
|
+
overlay.querySelector('[data-action="ok"]').addEventListener("click", close);
|
|
39
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Show a styled confirm dialog (replaces window.confirm).
|
|
45
|
+
* @param {string} title — dialog title / question
|
|
46
|
+
* @param {string} [detail] — optional detail text
|
|
47
|
+
* @param {object} [opts] — options: { confirmLabel, confirmClass }
|
|
48
|
+
* @returns {Promise<boolean>}
|
|
49
|
+
*/
|
|
50
|
+
function vibeConfirm(title, detail, opts) {
|
|
51
|
+
const label = (opts && opts.confirmLabel) || "Confirm";
|
|
52
|
+
const btnClass = (opts && opts.confirmClass) || "btn--danger";
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const overlay = document.createElement("div");
|
|
55
|
+
overlay.className = "confirm-overlay";
|
|
56
|
+
overlay.innerHTML = `
|
|
57
|
+
<div class="confirm-dialog">
|
|
58
|
+
<div class="confirm-dialog__title">${esc(title)}</div>
|
|
59
|
+
${detail ? `<p class="confirm-dialog__warn">${esc(detail)}</p>` : ""}
|
|
60
|
+
<div class="confirm-dialog__actions">
|
|
61
|
+
<button class="btn btn--secondary" data-action="cancel">Cancel</button>
|
|
62
|
+
<button class="btn ${btnClass}" data-action="confirm">${esc(label)}</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
`;
|
|
66
|
+
document.body.appendChild(overlay);
|
|
67
|
+
|
|
68
|
+
const close = (val) => { overlay.remove(); resolve(val); };
|
|
69
|
+
overlay.querySelector('[data-action="cancel"]').addEventListener("click", () => close(false));
|
|
70
|
+
overlay.querySelector('[data-action="confirm"]').addEventListener("click", () => close(true));
|
|
71
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(false); });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Show a styled prompt dialog with an input field (replaces window.prompt).
|
|
77
|
+
* @param {string} title — dialog title
|
|
78
|
+
* @param {string} [defaultValue] — pre-filled input value
|
|
79
|
+
* @param {string} [placeholder] — input placeholder
|
|
80
|
+
* @returns {Promise<string|null>} — input value or null if cancelled
|
|
81
|
+
*/
|
|
82
|
+
function vibePrompt(title, defaultValue, placeholder) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const overlay = document.createElement("div");
|
|
85
|
+
overlay.className = "confirm-overlay";
|
|
86
|
+
overlay.innerHTML = `
|
|
87
|
+
<div class="confirm-dialog">
|
|
88
|
+
<div class="confirm-dialog__title">${esc(title)}</div>
|
|
89
|
+
<input
|
|
90
|
+
type="text"
|
|
91
|
+
class="confirm-dialog__input"
|
|
92
|
+
value="${esc(defaultValue || "")}"
|
|
93
|
+
placeholder="${esc(placeholder || "")}"
|
|
94
|
+
data-role="input"
|
|
95
|
+
/>
|
|
96
|
+
<div class="confirm-dialog__actions">
|
|
97
|
+
<button class="btn btn--secondary" data-action="cancel">Cancel</button>
|
|
98
|
+
<button class="btn btn--primary" data-action="ok">OK</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
`;
|
|
102
|
+
document.body.appendChild(overlay);
|
|
103
|
+
|
|
104
|
+
const input = overlay.querySelector('[data-role="input"]');
|
|
105
|
+
setTimeout(() => { input.focus(); input.select(); }, 50);
|
|
106
|
+
|
|
107
|
+
const close = (val) => { overlay.remove(); resolve(val); };
|
|
108
|
+
|
|
109
|
+
overlay.querySelector('[data-action="cancel"]').addEventListener("click", () => close(null));
|
|
110
|
+
overlay.querySelector('[data-action="ok"]').addEventListener("click", () => close(input.value));
|
|
111
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(null); });
|
|
112
|
+
input.addEventListener("keydown", (e) => {
|
|
113
|
+
if (e.key === "Enter") close(input.value);
|
|
114
|
+
if (e.key === "Escape") close(null);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|