vibespot 1.6.5 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/assets/whats-new.json +27 -0
- package/dist/index.js +579 -681
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
- package/ui/chat.js +671 -9
- package/ui/dashboard.js +53 -12
- package/ui/docs/index.html +57 -2
- package/ui/email-preview.js +1 -5
- package/ui/escape-html.js +14 -0
- package/ui/field-editor.js +6 -12
- package/ui/field-save.js +82 -0
- package/ui/index.html +10 -4
- package/ui/inline-edit.js +116 -570
- package/ui/plan.js +22 -13
- package/ui/preview-agent.js +1050 -0
- package/ui/preview.js +248 -265
- package/ui/section-controls.js +16 -622
- package/ui/setup.js +73 -20
- package/ui/styles.css +424 -0
- package/ui/upload-panel.js +7 -8
- package/ui/whats-new.js +249 -0
- package/assets/readme/00-hero-banner.png +0 -0
- package/assets/readme/00-hero-banner.svg +0 -59
- package/assets/readme/01-vibe-coding-hero.png +0 -0
- package/assets/readme/02-plan-mode.png +0 -0
- package/assets/readme/03-figma-import.png +0 -0
- package/assets/readme/04-multi-page-sites.png +0 -0
- package/assets/readme/05-inline-wysiwyg.png +0 -0
- package/assets/readme/06-hubspot-upload.png +0 -0
- package/ui/docs/screenshots/brand-kit-preview.png +0 -0
- package/ui/docs/screenshots/content-type-dropdown.png +0 -0
- package/ui/docs/screenshots/editor-full-layout.png +0 -0
- package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
- package/ui/docs/screenshots/module-overview-slideout.png +0 -0
- package/ui/docs/screenshots/multi-page-tree.png +0 -0
- package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
- package/ui/docs/screenshots/split-pane-view.png +0 -0
- package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
- package/ui/docs/screenshots/workspace-tabs.png +0 -0
package/ui/preview.js
CHANGED
|
@@ -1,63 +1,140 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Preview management — iframe loading and module scrolling.
|
|
3
|
+
*
|
|
4
|
+
* VIB-1892: the live preview loads from a SEPARATE ORIGIN (see
|
|
5
|
+
* src/server/preview-origin.ts), so AI-generated page code is cross-origin to
|
|
6
|
+
* the app and can never touch this document or the app's /api routes. The
|
|
7
|
+
* parent never reaches into contentDocument; everything in-frame is done by
|
|
8
|
+
* the trusted preview agent over the narrow vs:* postMessage protocol
|
|
9
|
+
* (src/server/preview-protocol.ts). This file owns the parent end of that
|
|
10
|
+
* channel; inline-edit.js / section-controls.js register handlers on it.
|
|
3
11
|
*/
|
|
4
12
|
|
|
5
13
|
const previewFrame = document.getElementById("preview-frame");
|
|
6
14
|
const previewEmptyState = document.getElementById("preview-empty-state");
|
|
7
15
|
|
|
8
|
-
//
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Preview-origin channel (parent end)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const VS_PROTOCOL_V = 1;
|
|
21
|
+
/** Preview -> parent verbs the parent will dispatch. Locked to
|
|
22
|
+
* src/server/preview-protocol.ts by test/preview-protocol-parity.test.ts. */
|
|
23
|
+
const VS_PREVIEW_TO_PARENT = new Set([
|
|
24
|
+
"vs:ready",
|
|
25
|
+
"vs:empty-state",
|
|
26
|
+
"vs:select-module",
|
|
27
|
+
"vs:edit-commit",
|
|
28
|
+
"vs:field-commit",
|
|
29
|
+
"vs:request-mode",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** { origin, token } once /api/preview-origin resolves; null while loading. */
|
|
33
|
+
let previewOriginInfo = null;
|
|
34
|
+
let previewOriginPromise = null;
|
|
35
|
+
/** True between the agent's vs:ready and the next navigation. */
|
|
36
|
+
let previewAgentReady = false;
|
|
37
|
+
/** Bumped per navigation to force a fresh document load. */
|
|
38
|
+
let previewLoadSeq = 0;
|
|
39
|
+
/** Current interaction mode, re-sent to the agent on every (re)load. */
|
|
40
|
+
let currentPreviewMode = "view";
|
|
41
|
+
|
|
42
|
+
const previewMessageHandlers = new Map();
|
|
43
|
+
|
|
44
|
+
/** Register a parent-side handler for a preview->parent verb. */
|
|
45
|
+
function onPreviewMessage(type, handler) {
|
|
46
|
+
previewMessageHandlers.set(type, handler);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Send a command to the in-frame agent (drops silently until ready). */
|
|
50
|
+
function sendPreviewCommand(type, payload) {
|
|
51
|
+
if (!previewOriginInfo || !previewAgentReady || !previewFrame.contentWindow) return;
|
|
52
|
+
previewFrame.contentWindow.postMessage(
|
|
53
|
+
{ v: VS_PROTOCOL_V, token: previewOriginInfo.token, type, payload },
|
|
54
|
+
previewOriginInfo.origin
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Track + broadcast the interaction mode (called by inline-edit.js). */
|
|
59
|
+
function setPreviewMode(mode) {
|
|
60
|
+
currentPreviewMode = mode === "interact" || mode === "section" ? mode : "view";
|
|
61
|
+
sendPreviewCommand("vs:set-mode", { mode: currentPreviewMode });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fetchPreviewOriginInfo() {
|
|
65
|
+
if (previewOriginPromise) return previewOriginPromise;
|
|
66
|
+
previewOriginPromise = fetch("/api/preview-origin")
|
|
67
|
+
.then((res) => res.json())
|
|
68
|
+
.then((data) => {
|
|
69
|
+
if (data && typeof data.origin === "string" && typeof data.token === "string") {
|
|
70
|
+
previewOriginInfo = { origin: data.origin.replace(/\/$/, ""), token: data.token };
|
|
71
|
+
} else {
|
|
72
|
+
console.error("Preview origin unavailable — live preview disabled.");
|
|
73
|
+
}
|
|
74
|
+
return previewOriginInfo;
|
|
75
|
+
})
|
|
76
|
+
.catch((err) => {
|
|
77
|
+
console.error("Failed to resolve preview origin:", err);
|
|
78
|
+
previewOriginPromise = null; // allow a retry on the next refresh
|
|
79
|
+
return null;
|
|
80
|
+
});
|
|
81
|
+
return previewOriginPromise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
window.addEventListener("message", (event) => {
|
|
85
|
+
// Gate 1: the message must come from the preview origin AND from our frame.
|
|
86
|
+
if (!previewOriginInfo) return;
|
|
87
|
+
if (event.origin !== previewOriginInfo.origin) return;
|
|
88
|
+
if (event.source !== previewFrame.contentWindow) return;
|
|
89
|
+
// Gate 2: envelope — version, token, direction-scoped verb allow-list.
|
|
90
|
+
const env = event.data;
|
|
91
|
+
if (!env || typeof env !== "object") return;
|
|
92
|
+
if (env.v !== VS_PROTOCOL_V) return;
|
|
93
|
+
if (typeof env.token !== "string" || env.token !== previewOriginInfo.token) return;
|
|
94
|
+
if (!VS_PREVIEW_TO_PARENT.has(env.type)) return;
|
|
95
|
+
|
|
96
|
+
if (env.type === "vs:ready") {
|
|
97
|
+
previewAgentReady = true;
|
|
98
|
+
sendPreviewCommand("vs:init", { mode: currentPreviewMode });
|
|
99
|
+
flushPendingHighlights();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const handler = previewMessageHandlers.get(env.type);
|
|
103
|
+
if (handler) handler(env.payload || {});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Empty state + change highlighting
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
// Highlights to apply once the agent reports ready after the next refresh.
|
|
9
111
|
let pendingChangedModules = null;
|
|
10
112
|
let pendingNewModules = null;
|
|
11
113
|
|
|
12
114
|
/**
|
|
13
|
-
* Show or hide the preview empty state.
|
|
14
|
-
*
|
|
115
|
+
* Show or hide the preview empty state. Driven by the agent's vs:empty-state
|
|
116
|
+
* report after each load (the parent cannot inspect the cross-origin doc).
|
|
15
117
|
*/
|
|
16
118
|
function setPreviewEmptyState(show) {
|
|
17
119
|
if (!previewEmptyState) return;
|
|
18
120
|
previewEmptyState.setAttribute("aria-hidden", show ? "false" : "true");
|
|
19
121
|
}
|
|
20
122
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*/
|
|
25
|
-
function syncEmptyStateFromFrame() {
|
|
26
|
-
if (!previewEmptyState) return;
|
|
27
|
-
try {
|
|
28
|
-
const doc = previewFrame.contentDocument || previewFrame.contentWindow.document;
|
|
29
|
-
if (!doc || !doc.body) {
|
|
30
|
-
setPreviewEmptyState(true);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const hasModules = doc.querySelector("[data-module]") !== null;
|
|
34
|
-
setPreviewEmptyState(!hasModules);
|
|
35
|
-
} catch {
|
|
36
|
-
// cross-origin — assume content is present
|
|
37
|
-
setPreviewEmptyState(false);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
123
|
+
onPreviewMessage("vs:empty-state", (p) => {
|
|
124
|
+
setPreviewEmptyState(!p.hasModules);
|
|
125
|
+
});
|
|
40
126
|
|
|
41
|
-
|
|
42
|
-
syncEmptyStateFromFrame();
|
|
127
|
+
function flushPendingHighlights() {
|
|
43
128
|
if (!pendingChangedModules && !pendingNewModules) return;
|
|
44
129
|
const changed = pendingChangedModules;
|
|
45
130
|
const fresh = pendingNewModules;
|
|
46
131
|
pendingChangedModules = null;
|
|
47
132
|
pendingNewModules = null;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!doc || !doc.body) return;
|
|
51
|
-
ensureChangeHighlightStyles(doc);
|
|
52
|
-
if (fresh && fresh.length) animateNewModules(doc, fresh);
|
|
53
|
-
if (changed && changed.length) highlightChangedModules(doc, changed, fresh || []);
|
|
54
|
-
} catch {
|
|
55
|
-
// cross-origin — skip
|
|
56
|
-
}
|
|
57
|
-
});
|
|
133
|
+
sendPreviewCommand("vs:highlight-changed", { changed: changed || [], fresh: fresh || [] });
|
|
134
|
+
}
|
|
58
135
|
|
|
59
136
|
/**
|
|
60
|
-
* Refresh the preview iframe by
|
|
137
|
+
* Refresh the preview iframe by (re)navigating it to the preview origin.
|
|
61
138
|
*
|
|
62
139
|
* @param {Object} [opts]
|
|
63
140
|
* @param {string[]} [opts.changedModules] Module names that were just regenerated.
|
|
@@ -68,121 +145,44 @@ function refreshPreview(opts) {
|
|
|
68
145
|
pendingChangedModules = opts.changedModules || null;
|
|
69
146
|
pendingNewModules = opts.newModules || null;
|
|
70
147
|
}
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Change highlighting — outline glow on regenerated modules and slide-in for
|
|
85
|
-
// brand-new modules. Styles are injected into the sandboxed preview iframe.
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
|
|
88
|
-
function ensureChangeHighlightStyles(doc) {
|
|
89
|
-
if (doc.getElementById("vibespot-change-highlight-css")) return;
|
|
90
|
-
const style = doc.createElement("style");
|
|
91
|
-
style.id = "vibespot-change-highlight-css";
|
|
92
|
-
style.textContent = `
|
|
93
|
-
@keyframes vibespot-change-glow {
|
|
94
|
-
0% { outline-color: rgba(232, 97, 58, 0.85); box-shadow: 0 0 0 6px rgba(232, 97, 58, 0.18); }
|
|
95
|
-
70% { outline-color: rgba(232, 97, 58, 0.55); box-shadow: 0 0 0 4px rgba(232, 97, 58, 0.10); }
|
|
96
|
-
100% { outline-color: rgba(232, 97, 58, 0); box-shadow: 0 0 0 0 rgba(232, 97, 58, 0); }
|
|
97
|
-
}
|
|
98
|
-
.vibespot-module--changed {
|
|
99
|
-
outline: 2px solid rgba(232, 97, 58, 0.85);
|
|
100
|
-
outline-offset: 4px;
|
|
101
|
-
border-radius: 2px;
|
|
102
|
-
animation: vibespot-change-glow 2s ease-out forwards;
|
|
103
|
-
}
|
|
104
|
-
@keyframes vibespot-module-slide-in {
|
|
105
|
-
0% { opacity: 0; transform: translateY(24px); }
|
|
106
|
-
100% { opacity: 1; transform: translateY(0); }
|
|
107
|
-
}
|
|
108
|
-
.vibespot-module--new {
|
|
109
|
-
animation: vibespot-module-slide-in 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
110
|
-
}
|
|
111
|
-
@media (prefers-reduced-motion: reduce) {
|
|
112
|
-
.vibespot-module--changed,
|
|
113
|
-
.vibespot-module--new { animation: none; }
|
|
114
|
-
.vibespot-module--changed { outline-color: transparent; }
|
|
148
|
+
// The origin lookup is async — only the newest requested navigation may
|
|
149
|
+
// touch the frame, or a slow resolve clobbers a later refresh or the
|
|
150
|
+
// generating screen (VIB-1898).
|
|
151
|
+
previewLoadSeq += 1;
|
|
152
|
+
const nav = previewLoadSeq;
|
|
153
|
+
fetchPreviewOriginInfo().then((info) => {
|
|
154
|
+
if (nav !== previewLoadSeq) return; // superseded while resolving
|
|
155
|
+
if (!info) {
|
|
156
|
+
// The preview origin never resolved (stale cached client hitting a
|
|
157
|
+
// moved route, the origin process not running, or the port blocked).
|
|
158
|
+
// Surface a self-diagnosing message instead of a silent blank frame.
|
|
159
|
+
showPreviewUnavailable();
|
|
160
|
+
return;
|
|
115
161
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// Skip modules that are already getting the slide-in animation — the slide-in
|
|
122
|
-
// is a stronger signal on its own.
|
|
123
|
-
const newSet = new Set(newModuleNames);
|
|
124
|
-
for (const name of moduleNames) {
|
|
125
|
-
if (newSet.has(name)) continue;
|
|
126
|
-
const el = doc.querySelector(`[data-module="${cssEscape(name)}"]`);
|
|
127
|
-
if (!el) continue;
|
|
128
|
-
el.classList.remove("vibespot-module--changed");
|
|
129
|
-
// Force a reflow so the animation restarts if the class was just removed.
|
|
130
|
-
void el.offsetWidth;
|
|
131
|
-
el.classList.add("vibespot-module--changed");
|
|
132
|
-
setTimeout(() => {
|
|
133
|
-
el.classList.remove("vibespot-module--changed");
|
|
134
|
-
}, 2200);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function animateNewModules(doc, moduleNames) {
|
|
139
|
-
for (const name of moduleNames) {
|
|
140
|
-
const el = doc.querySelector(`[data-module="${cssEscape(name)}"]`);
|
|
141
|
-
if (!el) continue;
|
|
142
|
-
el.classList.add("vibespot-module--new");
|
|
143
|
-
setTimeout(() => {
|
|
144
|
-
el.classList.remove("vibespot-module--new");
|
|
145
|
-
}, 700);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function cssEscape(value) {
|
|
150
|
-
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
151
|
-
return CSS.escape(value);
|
|
152
|
-
}
|
|
153
|
-
return String(value).replace(/["\\]/g, "\\$&");
|
|
162
|
+
previewAgentReady = false;
|
|
163
|
+
// The `r` param defeats bfcache/no-op navigations; `t` is the access token.
|
|
164
|
+
previewFrame.removeAttribute("srcdoc");
|
|
165
|
+
previewFrame.src = `${info.origin}/?t=${encodeURIComponent(info.token)}&r=${nav}`;
|
|
166
|
+
});
|
|
154
167
|
}
|
|
155
168
|
|
|
156
169
|
/**
|
|
157
170
|
* Scroll the preview iframe to a specific module by name.
|
|
158
171
|
*/
|
|
159
172
|
function scrollPreviewToModule(moduleName) {
|
|
160
|
-
|
|
161
|
-
const doc = previewFrame.contentDocument || previewFrame.contentWindow.document;
|
|
162
|
-
const el = doc.querySelector(`[data-module="${moduleName}"]`);
|
|
163
|
-
if (el) {
|
|
164
|
-
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
165
|
-
|
|
166
|
-
// Brief highlight effect
|
|
167
|
-
el.style.outline = "2px solid #e8613a";
|
|
168
|
-
el.style.outlineOffset = "4px";
|
|
169
|
-
el.style.transition = "outline-color 0.5s ease";
|
|
170
|
-
setTimeout(() => {
|
|
171
|
-
el.style.outlineColor = "transparent";
|
|
172
|
-
setTimeout(() => { el.style.outline = ""; el.style.outlineOffset = ""; }, 500);
|
|
173
|
-
}, 1500);
|
|
174
|
-
}
|
|
175
|
-
} catch {
|
|
176
|
-
// Cross-origin issues — fall back to simple reload
|
|
177
|
-
}
|
|
173
|
+
sendPreviewCommand("vs:scroll-to", { module: moduleName });
|
|
178
174
|
}
|
|
179
175
|
|
|
180
176
|
/**
|
|
181
177
|
* Show the generating preview — spinner + rotating fun messages.
|
|
182
|
-
*
|
|
178
|
+
* Trusted static content (no AI code), so srcdoc is fine here.
|
|
183
179
|
*/
|
|
184
180
|
function showGeneratingPreview() {
|
|
185
181
|
setPreviewEmptyState(false);
|
|
182
|
+
previewAgentReady = false;
|
|
183
|
+
// Invalidate any refresh navigation still resolving its origin lookup so it
|
|
184
|
+
// can't replace this screen after we show it (VIB-1898).
|
|
185
|
+
previewLoadSeq += 1;
|
|
186
186
|
const html = `<!DOCTYPE html>
|
|
187
187
|
<html lang="en">
|
|
188
188
|
<head>
|
|
@@ -259,161 +259,144 @@ function showGeneratingPreview() {
|
|
|
259
259
|
<\/script>
|
|
260
260
|
</body>
|
|
261
261
|
</html>`;
|
|
262
|
+
previewFrame.removeAttribute("src");
|
|
262
263
|
previewFrame.srcdoc = html;
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// "Working on it" overlay for modules being regenerated
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
|
|
269
|
-
const WORKING_MESSAGES = [
|
|
270
|
-
"Rethinking this section\u2026",
|
|
271
|
-
"Rewriting the code\u2026",
|
|
272
|
-
"Giving this a fresh coat of paint\u2026",
|
|
273
|
-
"Making it better\u2026",
|
|
274
|
-
"Tweaking the layout\u2026",
|
|
275
|
-
"Polishing the details\u2026",
|
|
276
|
-
"Almost there\u2026",
|
|
277
|
-
"Updating the design\u2026",
|
|
278
|
-
"Improving the copy\u2026",
|
|
279
|
-
"Refining the structure\u2026",
|
|
280
|
-
];
|
|
281
|
-
|
|
282
266
|
/**
|
|
283
|
-
*
|
|
267
|
+
* Show a visible in-frame fallback when the preview origin can't be resolved
|
|
268
|
+
* (`/api/preview-origin` returned `{origin:null}` or the fetch failed). Without
|
|
269
|
+
* this the frame just stays blank and the failure is invisible — the classic
|
|
270
|
+
* symptom of a stale cached `ui/preview.js` running against an upgraded server,
|
|
271
|
+
* or the preview origin process not being reachable (VIB-1937). Trusted static
|
|
272
|
+
* content (no AI code), so srcdoc is fine here.
|
|
284
273
|
*/
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
274
|
+
function showPreviewUnavailable() {
|
|
275
|
+
previewAgentReady = false;
|
|
276
|
+
// Hide the opaque empty-state overlay — it defaults to visible (aria-hidden
|
|
277
|
+
// false, solid background) and otherwise paints on top of the fallback card
|
|
278
|
+
// rendered into the iframe below, masking it (VIB-1938).
|
|
279
|
+
setPreviewEmptyState(false);
|
|
280
|
+
// Compute the expected preview port (app port + 2) so the hint is concrete.
|
|
281
|
+
const appPort = Number(
|
|
282
|
+
window.location.port || (window.location.protocol === "https:" ? 443 : 80)
|
|
283
|
+
);
|
|
284
|
+
const previewPort = Number.isFinite(appPort) ? appPort + 2 : null;
|
|
285
|
+
const portHint = previewPort
|
|
286
|
+
? `the preview port (<code>${previewPort}</code>, i.e. app port + 2)`
|
|
287
|
+
: "the preview port (app port + 2)";
|
|
288
|
+
const html = `<!DOCTYPE html>
|
|
289
|
+
<html lang="en">
|
|
290
|
+
<head>
|
|
291
|
+
<meta charset="utf-8">
|
|
292
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
293
|
+
<style>
|
|
294
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
295
|
+
body {
|
|
296
|
+
min-height: 100vh;
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: center;
|
|
299
|
+
justify-content: center;
|
|
300
|
+
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
301
|
+
background: #0c0a09;
|
|
302
|
+
color: rgba(255,255,255,0.62);
|
|
303
|
+
padding: 2rem;
|
|
304
|
+
}
|
|
305
|
+
.unavail {
|
|
306
|
+
text-align: center;
|
|
307
|
+
max-width: 460px;
|
|
308
|
+
}
|
|
309
|
+
.unavail .icon {
|
|
310
|
+
font-size: 2.2rem;
|
|
311
|
+
margin-bottom: 1rem;
|
|
312
|
+
opacity: 0.7;
|
|
313
|
+
}
|
|
314
|
+
.unavail h1 {
|
|
315
|
+
font-size: 1.15rem;
|
|
316
|
+
font-weight: 600;
|
|
317
|
+
color: rgba(255,255,255,0.9);
|
|
318
|
+
margin-bottom: 0.75rem;
|
|
319
|
+
}
|
|
320
|
+
.unavail p {
|
|
321
|
+
font-size: 0.95rem;
|
|
322
|
+
line-height: 1.6;
|
|
323
|
+
margin-bottom: 0.6rem;
|
|
324
|
+
}
|
|
325
|
+
.unavail ul {
|
|
326
|
+
text-align: left;
|
|
327
|
+
display: inline-block;
|
|
328
|
+
margin: 0.4rem auto 0;
|
|
329
|
+
padding-left: 1.1rem;
|
|
330
|
+
font-size: 0.9rem;
|
|
331
|
+
line-height: 1.7;
|
|
332
|
+
}
|
|
333
|
+
.unavail code {
|
|
334
|
+
font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
|
|
335
|
+
font-size: 0.85em;
|
|
336
|
+
background: rgba(255,255,255,0.08);
|
|
337
|
+
padding: 0.1em 0.4em;
|
|
338
|
+
border-radius: 4px;
|
|
339
|
+
color: rgba(232, 97, 58, 0.95);
|
|
340
|
+
}
|
|
341
|
+
.unavail kbd {
|
|
342
|
+
font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
|
|
343
|
+
font-size: 0.8em;
|
|
344
|
+
background: rgba(255,255,255,0.1);
|
|
345
|
+
border: 1px solid rgba(255,255,255,0.15);
|
|
346
|
+
border-bottom-width: 2px;
|
|
347
|
+
padding: 0.1em 0.4em;
|
|
348
|
+
border-radius: 4px;
|
|
349
|
+
white-space: nowrap;
|
|
350
|
+
}
|
|
351
|
+
</style>
|
|
352
|
+
</head>
|
|
353
|
+
<body>
|
|
354
|
+
<div class="unavail">
|
|
355
|
+
<div class="icon">⚠</div>
|
|
356
|
+
<h1>Live preview unavailable</h1>
|
|
357
|
+
<p>The editor couldn't reach the preview server, so the page can't render here yet.</p>
|
|
358
|
+
<p>Two things usually fix it:</p>
|
|
359
|
+
<ul>
|
|
360
|
+
<li>Hard-refresh this page (<kbd>Cmd/Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd>) to load the latest editor after an upgrade.</li>
|
|
361
|
+
<li>Make sure ${portHint} is free and reachable.</li>
|
|
362
|
+
</ul>
|
|
363
|
+
</div>
|
|
364
|
+
</body>
|
|
365
|
+
</html>`;
|
|
366
|
+
previewFrame.removeAttribute("src");
|
|
367
|
+
previewFrame.srcdoc = html;
|
|
324
368
|
}
|
|
325
369
|
|
|
326
|
-
|
|
327
|
-
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// "Working on it" overlays — forwarded to the in-frame agent
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
328
373
|
|
|
329
374
|
/**
|
|
330
|
-
* Mark modules as "being worked on" — adds blur overlay with
|
|
375
|
+
* Mark modules as "being worked on" — the agent adds the blur overlay with
|
|
376
|
+
* rotating messages in-frame.
|
|
331
377
|
*/
|
|
332
378
|
function markModulesWorking(moduleNames) {
|
|
333
|
-
|
|
334
|
-
const doc = previewFrame.contentDocument || previewFrame.contentWindow.document;
|
|
335
|
-
if (!doc || !doc.body) return;
|
|
336
|
-
ensureOverlayStyles(doc);
|
|
337
|
-
|
|
338
|
-
for (const name of moduleNames) {
|
|
339
|
-
const el = doc.querySelector(`[data-module="${name}"]`);
|
|
340
|
-
if (!el || el.classList.contains("vibespot-module--working")) continue;
|
|
341
|
-
|
|
342
|
-
el.classList.add("vibespot-module--working");
|
|
343
|
-
|
|
344
|
-
const overlay = doc.createElement("div");
|
|
345
|
-
overlay.className = "vibespot-working-overlay";
|
|
346
|
-
overlay.innerHTML = `
|
|
347
|
-
<div class="vibespot-working-overlay__spinner"></div>
|
|
348
|
-
<div class="vibespot-working-overlay__text"></div>
|
|
349
|
-
`;
|
|
350
|
-
el.appendChild(overlay);
|
|
351
|
-
|
|
352
|
-
// Scroll to first working module
|
|
353
|
-
if (name === moduleNames[0]) {
|
|
354
|
-
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Start message carousel
|
|
358
|
-
const textEl = overlay.querySelector(".vibespot-working-overlay__text");
|
|
359
|
-
let idx = Math.floor(Math.random() * WORKING_MESSAGES.length);
|
|
360
|
-
textEl.textContent = WORKING_MESSAGES[idx];
|
|
361
|
-
|
|
362
|
-
const interval = setInterval(() => {
|
|
363
|
-
textEl.classList.add("fade");
|
|
364
|
-
setTimeout(() => {
|
|
365
|
-
idx = (idx + 1) % WORKING_MESSAGES.length;
|
|
366
|
-
textEl.textContent = WORKING_MESSAGES[idx];
|
|
367
|
-
textEl.classList.remove("fade");
|
|
368
|
-
}, 500);
|
|
369
|
-
}, 3500);
|
|
370
|
-
workingIntervals.set(name, interval);
|
|
371
|
-
}
|
|
372
|
-
} catch { /* cross-origin */ }
|
|
379
|
+
sendPreviewCommand("vs:mark-working", { modules: moduleNames });
|
|
373
380
|
}
|
|
374
381
|
|
|
375
382
|
/**
|
|
376
383
|
* Clear working overlay from a specific module (when it completes).
|
|
377
384
|
*/
|
|
378
385
|
function clearModuleWorking(moduleName) {
|
|
379
|
-
|
|
380
|
-
if (interval) { clearInterval(interval); workingIntervals.delete(moduleName); }
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
const doc = previewFrame.contentDocument || previewFrame.contentWindow.document;
|
|
384
|
-
if (!doc) return;
|
|
385
|
-
const el = doc.querySelector(`[data-module="${moduleName}"]`);
|
|
386
|
-
if (!el) return;
|
|
387
|
-
el.classList.remove("vibespot-module--working");
|
|
388
|
-
const overlay = el.querySelector(".vibespot-working-overlay");
|
|
389
|
-
if (overlay) overlay.remove();
|
|
390
|
-
} catch { /* cross-origin */ }
|
|
386
|
+
sendPreviewCommand("vs:clear-working", { modules: [moduleName] });
|
|
391
387
|
}
|
|
392
388
|
|
|
393
389
|
/**
|
|
394
390
|
* Clear all working overlays.
|
|
395
391
|
*/
|
|
396
392
|
function clearAllModulesWorking() {
|
|
397
|
-
|
|
398
|
-
clearInterval(interval);
|
|
399
|
-
}
|
|
400
|
-
workingIntervals.clear();
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
const doc = previewFrame.contentDocument || previewFrame.contentWindow.document;
|
|
404
|
-
if (!doc) return;
|
|
405
|
-
doc.querySelectorAll(".vibespot-module--working").forEach((el) => {
|
|
406
|
-
el.classList.remove("vibespot-module--working");
|
|
407
|
-
const overlay = el.querySelector(".vibespot-working-overlay");
|
|
408
|
-
if (overlay) overlay.remove();
|
|
409
|
-
});
|
|
410
|
-
} catch { /* cross-origin */ }
|
|
393
|
+
sendPreviewCommand("vs:clear-working", {});
|
|
411
394
|
}
|
|
412
395
|
|
|
413
396
|
// Preview refresh is triggered by setup.js after a session is created.
|
|
414
|
-
// Do NOT auto-refresh here
|
|
415
|
-
|
|
416
|
-
|
|
397
|
+
// Do NOT auto-refresh here — but resolve the preview origin early so the
|
|
398
|
+
// first refresh doesn't pay the round-trip.
|
|
399
|
+
fetchPreviewOriginInfo();
|
|
417
400
|
|
|
418
401
|
// ---------------------------------------------------------------------------
|
|
419
402
|
// HubL validity badge — aggregates per-module checks reported by chat.js into
|