vibespot 1.7.7 → 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 +11 -0
- package/assets/whats-new.json +27 -0
- package/dist/index.js +482 -648
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
- package/ui/chat.js +21 -8
- package/ui/dashboard.js +53 -12
- package/ui/docs/index.html +10 -1
- 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 +6 -3
- package/ui/inline-edit.js +116 -570
- package/ui/plan.js +19 -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 +177 -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-checkpoints.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/checkpoint-card.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/setup.js
CHANGED
|
@@ -136,19 +136,27 @@ async function initSetup() {
|
|
|
136
136
|
// Show "Continue where you left off" cards above the create options
|
|
137
137
|
populateRecentProjects(info);
|
|
138
138
|
|
|
139
|
-
// Auto-select engine if available but not yet chosen
|
|
139
|
+
// Auto-select engine if available but not yet chosen. Guarded on its own:
|
|
140
|
+
// a transient failure here just leaves the "no engine" alert visible —
|
|
141
|
+
// it must not abort the rest of setup rendering.
|
|
140
142
|
if (info.availableEngines && info.availableEngines.length > 0 && !info.activeEngine) {
|
|
141
143
|
const engine = info.availableEngines[0];
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
try {
|
|
145
|
+
const engineRes = await fetch("/api/settings/engine", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
body: JSON.stringify({ engine }),
|
|
149
|
+
});
|
|
150
|
+
if (engineRes.ok) {
|
|
151
|
+
info.activeEngine = engine;
|
|
152
|
+
info.aiAvailable = true;
|
|
153
|
+
// Update statusbar
|
|
154
|
+
const statusEngine = document.getElementById("status-engine");
|
|
155
|
+
if (statusEngine) statusEngine.textContent = ENGINE_DISPLAY_NAMES[engine] || engine;
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.warn("[setup] engine auto-select failed", err);
|
|
159
|
+
}
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
// Show environment alerts
|
|
@@ -1069,25 +1077,48 @@ async function handleCliAction(engineId, action, btn) {
|
|
|
1069
1077
|
const data = await res.json();
|
|
1070
1078
|
if (data.jobId) {
|
|
1071
1079
|
// Poll until complete
|
|
1072
|
-
await
|
|
1080
|
+
await pollJobUntilDone(data.jobId);
|
|
1073
1081
|
}
|
|
1074
1082
|
// Refresh walkthrough to show updated status
|
|
1075
1083
|
showWalkthrough();
|
|
1076
|
-
} catch {
|
|
1084
|
+
} catch (err) {
|
|
1077
1085
|
btn.disabled = false;
|
|
1078
1086
|
btn.textContent = origText;
|
|
1087
|
+
await vibeAlert((action === "install" ? "Install failed: " : "Sign-in failed: ") + (err.message || err), "Error");
|
|
1079
1088
|
}
|
|
1080
1089
|
}
|
|
1081
1090
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1091
|
+
// Poll a background job until it finishes. Resolves with the job payload on
|
|
1092
|
+
// success; throws on failure, on a vanished job (404 — the server restarted
|
|
1093
|
+
// or GC'd it, so it will never complete), and on timeout — so callers can't
|
|
1094
|
+
// spin forever on a dead job. Also used by upload-panel.js.
|
|
1095
|
+
// NOT named pollJob: settings.js declares a legacy callback-style pollJob in
|
|
1096
|
+
// the same global scope and loads later, so that name gets shadowed (VIB-1931).
|
|
1097
|
+
async function pollJobUntilDone(jobId, { timeoutMs = 5 * 60 * 1000, intervalMs = 2000 } = {}) {
|
|
1098
|
+
const deadline = Date.now() + timeoutMs;
|
|
1099
|
+
while (Date.now() < deadline) {
|
|
1100
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1101
|
+
let res;
|
|
1085
1102
|
try {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1103
|
+
res = await fetch("/api/settings/job/" + jobId);
|
|
1104
|
+
} catch {
|
|
1105
|
+
continue; // transient network error — keep polling until the deadline
|
|
1106
|
+
}
|
|
1107
|
+
if (res.status === 404) {
|
|
1108
|
+
throw new Error("The background job was lost (server restarted?). Please try again.");
|
|
1109
|
+
}
|
|
1110
|
+
let data;
|
|
1111
|
+
try {
|
|
1112
|
+
data = await res.json();
|
|
1113
|
+
} catch {
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
if (data.status === "completed") return data;
|
|
1117
|
+
if (data.status === "failed") {
|
|
1118
|
+
throw new Error(data.output ? String(data.output).slice(-500) : "The background job failed.");
|
|
1119
|
+
}
|
|
1090
1120
|
}
|
|
1121
|
+
throw new Error("Timed out waiting for the job to finish. Please try again.");
|
|
1091
1122
|
}
|
|
1092
1123
|
|
|
1093
1124
|
// ---------------------------------------------------------------------------
|
|
@@ -2123,6 +2154,17 @@ document.getElementById("figma-extract-btn")?.addEventListener("click", async ()
|
|
|
2123
2154
|
body: JSON.stringify({ url }),
|
|
2124
2155
|
});
|
|
2125
2156
|
|
|
2157
|
+
// Non-streaming failure (auth gate, 4xx/5xx JSON error) — surface it
|
|
2158
|
+
// instead of letting getReader() throw an opaque TypeError.
|
|
2159
|
+
if (!res.ok || !res.body) {
|
|
2160
|
+
let msg = "Extraction failed (" + res.status + ")";
|
|
2161
|
+
try {
|
|
2162
|
+
const errData = await res.json();
|
|
2163
|
+
if (errData.error) msg = errData.error;
|
|
2164
|
+
} catch { /* non-JSON error body */ }
|
|
2165
|
+
throw new Error(msg);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2126
2168
|
const reader = res.body.getReader();
|
|
2127
2169
|
const decoder = new TextDecoder();
|
|
2128
2170
|
let buffer = "";
|
|
@@ -2280,6 +2322,17 @@ async function startFigmaImport(extractionId, themeName, useAssets = true) {
|
|
|
2280
2322
|
body: JSON.stringify({ extractionId, themeName, useAssets }),
|
|
2281
2323
|
});
|
|
2282
2324
|
|
|
2325
|
+
// Non-streaming failure (auth gate, 4xx/5xx JSON error) — surface it
|
|
2326
|
+
// instead of letting getReader() throw an opaque TypeError.
|
|
2327
|
+
if (!res.ok || !res.body) {
|
|
2328
|
+
let msg = "Conversion failed (" + res.status + ")";
|
|
2329
|
+
try {
|
|
2330
|
+
const errData = await res.json();
|
|
2331
|
+
if (errData.error) msg = errData.error;
|
|
2332
|
+
} catch { /* non-JSON error body */ }
|
|
2333
|
+
throw new Error(msg);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2283
2336
|
const reader = res.body.getReader();
|
|
2284
2337
|
const decoder = new TextDecoder();
|
|
2285
2338
|
let buffer = "";
|
package/ui/styles.css
CHANGED
|
@@ -8463,3 +8463,180 @@ body { display: flex; }
|
|
|
8463
8463
|
margin-top: var(--space-1);
|
|
8464
8464
|
}
|
|
8465
8465
|
.brand-intake__top-actions { margin-top: var(--space-2); }
|
|
8466
|
+
|
|
8467
|
+
/* ===========================================================================
|
|
8468
|
+
"What's new" release dialog (VIB-1886)
|
|
8469
|
+
One-time post-upgrade modal. Reuses overlay/dialog tokens — no one-off values.
|
|
8470
|
+
=========================================================================== */
|
|
8471
|
+
.whatsnew-overlay {
|
|
8472
|
+
position: fixed;
|
|
8473
|
+
inset: 0;
|
|
8474
|
+
background: var(--overlay-bg);
|
|
8475
|
+
z-index: var(--z-confirm);
|
|
8476
|
+
display: flex;
|
|
8477
|
+
align-items: center;
|
|
8478
|
+
justify-content: center;
|
|
8479
|
+
padding: var(--space-5);
|
|
8480
|
+
animation: fadeIn var(--duration-fast) var(--ease-default);
|
|
8481
|
+
}
|
|
8482
|
+
|
|
8483
|
+
.whatsnew-dialog {
|
|
8484
|
+
position: relative;
|
|
8485
|
+
background: var(--bg-panel-solid);
|
|
8486
|
+
border: 1px solid var(--border);
|
|
8487
|
+
border-radius: var(--radius-lg);
|
|
8488
|
+
padding: var(--space-6);
|
|
8489
|
+
width: 460px;
|
|
8490
|
+
max-width: 100%;
|
|
8491
|
+
max-height: calc(100vh - var(--space-8));
|
|
8492
|
+
overflow-y: auto;
|
|
8493
|
+
box-shadow: var(--shadow-xl);
|
|
8494
|
+
animation: whatsnewIn var(--duration-slow) var(--ease-spring);
|
|
8495
|
+
}
|
|
8496
|
+
|
|
8497
|
+
@keyframes whatsnewIn {
|
|
8498
|
+
from { opacity: 0; transform: translateY(8px) scale(0.98); }
|
|
8499
|
+
to { opacity: 1; transform: none; }
|
|
8500
|
+
}
|
|
8501
|
+
|
|
8502
|
+
.whatsnew__close {
|
|
8503
|
+
position: absolute;
|
|
8504
|
+
top: var(--space-4);
|
|
8505
|
+
right: var(--space-4);
|
|
8506
|
+
width: 32px;
|
|
8507
|
+
height: 32px;
|
|
8508
|
+
display: inline-flex;
|
|
8509
|
+
align-items: center;
|
|
8510
|
+
justify-content: center;
|
|
8511
|
+
background: transparent;
|
|
8512
|
+
border: none;
|
|
8513
|
+
border-radius: var(--radius-sm);
|
|
8514
|
+
color: var(--text-dim);
|
|
8515
|
+
cursor: pointer;
|
|
8516
|
+
transition: color var(--duration-fast) var(--ease-default),
|
|
8517
|
+
background var(--duration-fast) var(--ease-default);
|
|
8518
|
+
}
|
|
8519
|
+
.whatsnew__close:hover { color: var(--text); background: var(--bg-hover); }
|
|
8520
|
+
.whatsnew__close:focus-visible { box-shadow: var(--shadow-focus); outline: none; }
|
|
8521
|
+
|
|
8522
|
+
/* Brand lockup — accent star on its dark container (brand rule: container intact) */
|
|
8523
|
+
.whatsnew__brand {
|
|
8524
|
+
display: flex;
|
|
8525
|
+
align-items: center;
|
|
8526
|
+
gap: var(--space-2);
|
|
8527
|
+
margin-bottom: var(--space-5);
|
|
8528
|
+
}
|
|
8529
|
+
.whatsnew__logo {
|
|
8530
|
+
width: 40px;
|
|
8531
|
+
height: 40px;
|
|
8532
|
+
display: inline-flex;
|
|
8533
|
+
align-items: center;
|
|
8534
|
+
justify-content: center;
|
|
8535
|
+
background: var(--bg);
|
|
8536
|
+
border: 1px solid var(--border);
|
|
8537
|
+
border-radius: var(--radius);
|
|
8538
|
+
}
|
|
8539
|
+
.whatsnew__wordmark {
|
|
8540
|
+
font-family: var(--font-display);
|
|
8541
|
+
font-size: var(--text-lg);
|
|
8542
|
+
font-weight: var(--weight-semibold);
|
|
8543
|
+
letter-spacing: var(--tracking-tight);
|
|
8544
|
+
color: var(--text);
|
|
8545
|
+
}
|
|
8546
|
+
.whatsnew__version {
|
|
8547
|
+
font-family: var(--font-mono);
|
|
8548
|
+
font-size: var(--text-xs);
|
|
8549
|
+
font-weight: var(--weight-medium);
|
|
8550
|
+
letter-spacing: var(--tracking-wide);
|
|
8551
|
+
color: var(--accent);
|
|
8552
|
+
background: var(--accent-tint);
|
|
8553
|
+
border: 1px solid var(--accent-dim);
|
|
8554
|
+
border-radius: var(--radius-pill);
|
|
8555
|
+
padding: 2px var(--space-2);
|
|
8556
|
+
}
|
|
8557
|
+
|
|
8558
|
+
.whatsnew__title {
|
|
8559
|
+
font-family: var(--font-display);
|
|
8560
|
+
font-size: var(--text-2xl);
|
|
8561
|
+
font-weight: var(--weight-bold);
|
|
8562
|
+
line-height: var(--leading-tight);
|
|
8563
|
+
letter-spacing: var(--tracking-tight);
|
|
8564
|
+
color: var(--text);
|
|
8565
|
+
margin: 0 0 var(--space-1);
|
|
8566
|
+
}
|
|
8567
|
+
.whatsnew__date {
|
|
8568
|
+
font-size: var(--text-sm);
|
|
8569
|
+
color: var(--text-dim);
|
|
8570
|
+
margin: 0 0 var(--space-5);
|
|
8571
|
+
}
|
|
8572
|
+
|
|
8573
|
+
.whatsnew__list {
|
|
8574
|
+
list-style: none;
|
|
8575
|
+
margin: 0 0 var(--space-6);
|
|
8576
|
+
padding: 0;
|
|
8577
|
+
display: flex;
|
|
8578
|
+
flex-direction: column;
|
|
8579
|
+
gap: var(--space-4);
|
|
8580
|
+
}
|
|
8581
|
+
.whatsnew__item {
|
|
8582
|
+
display: flex;
|
|
8583
|
+
align-items: flex-start;
|
|
8584
|
+
gap: var(--space-3);
|
|
8585
|
+
}
|
|
8586
|
+
.whatsnew__marker {
|
|
8587
|
+
flex: 0 0 auto;
|
|
8588
|
+
width: 28px;
|
|
8589
|
+
height: 28px;
|
|
8590
|
+
display: inline-flex;
|
|
8591
|
+
align-items: center;
|
|
8592
|
+
justify-content: center;
|
|
8593
|
+
background: var(--accent-tint);
|
|
8594
|
+
border-radius: var(--radius-sm);
|
|
8595
|
+
}
|
|
8596
|
+
.whatsnew__marker svg { width: 16px; height: 16px; }
|
|
8597
|
+
.whatsnew__item-text { display: flex; flex-direction: column; gap: 2px; }
|
|
8598
|
+
.whatsnew__item-title {
|
|
8599
|
+
font-size: var(--text-md);
|
|
8600
|
+
font-weight: var(--weight-semibold);
|
|
8601
|
+
color: var(--text);
|
|
8602
|
+
line-height: var(--leading-tight);
|
|
8603
|
+
}
|
|
8604
|
+
.whatsnew__item-body {
|
|
8605
|
+
font-size: var(--text-base);
|
|
8606
|
+
line-height: var(--leading-relaxed);
|
|
8607
|
+
color: var(--text-dim);
|
|
8608
|
+
}
|
|
8609
|
+
|
|
8610
|
+
.whatsnew__actions {
|
|
8611
|
+
display: flex;
|
|
8612
|
+
justify-content: flex-end;
|
|
8613
|
+
gap: var(--space-3);
|
|
8614
|
+
}
|
|
8615
|
+
.whatsnew__actions .btn { text-decoration: none; }
|
|
8616
|
+
|
|
8617
|
+
/* Narrow widths — modal becomes a bottom-anchored sheet, actions stack full-width */
|
|
8618
|
+
@media (max-width: 520px) {
|
|
8619
|
+
.whatsnew-overlay { align-items: flex-end; padding: 0; }
|
|
8620
|
+
.whatsnew-dialog {
|
|
8621
|
+
width: 100%;
|
|
8622
|
+
max-width: 100%;
|
|
8623
|
+
max-height: 90vh;
|
|
8624
|
+
padding: var(--space-5);
|
|
8625
|
+
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
8626
|
+
border-bottom: none;
|
|
8627
|
+
animation: whatsnewSheetIn var(--duration-slow) var(--ease-out);
|
|
8628
|
+
}
|
|
8629
|
+
.whatsnew__title { font-size: var(--text-xl); }
|
|
8630
|
+
.whatsnew__actions { flex-direction: column-reverse; }
|
|
8631
|
+
.whatsnew__actions .btn { width: 100%; }
|
|
8632
|
+
}
|
|
8633
|
+
|
|
8634
|
+
@keyframes whatsnewSheetIn {
|
|
8635
|
+
from { opacity: 0; transform: translateY(100%); }
|
|
8636
|
+
to { opacity: 1; transform: none; }
|
|
8637
|
+
}
|
|
8638
|
+
|
|
8639
|
+
@media (prefers-reduced-motion: reduce) {
|
|
8640
|
+
.whatsnew-overlay,
|
|
8641
|
+
.whatsnew-dialog { animation: none; }
|
|
8642
|
+
}
|
package/ui/upload-panel.js
CHANGED
|
@@ -116,16 +116,15 @@ function showHubSpotSetupDialog(mode) {
|
|
|
116
116
|
});
|
|
117
117
|
const data = await res.json();
|
|
118
118
|
if (data.jobId) {
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (jr.status === "failed") { close(false); return; }
|
|
125
|
-
}
|
|
119
|
+
// pollJobUntilDone (setup.js) resolves on success and throws on
|
|
120
|
+
// failure / lost job (404) / timeout.
|
|
121
|
+
await pollJobUntilDone(data.jobId);
|
|
122
|
+
close(true);
|
|
123
|
+
return;
|
|
126
124
|
}
|
|
127
125
|
close(false);
|
|
128
|
-
} catch {
|
|
126
|
+
} catch (err) {
|
|
127
|
+
await vibeAlert("Install failed: " + (err.message || err), "Error");
|
|
129
128
|
close(false);
|
|
130
129
|
}
|
|
131
130
|
});
|
package/ui/whats-new.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "What's new" release dialog — shown once after a version upgrade.
|
|
3
|
+
*
|
|
4
|
+
* SCAFFOLD (VIB-1886): UX + clickable demo only. Production gating
|
|
5
|
+
* (lastSeenVersion read/write, server route) is a follow-up owned by CTO.
|
|
6
|
+
*
|
|
7
|
+
* Mount points:
|
|
8
|
+
* - Production seam: maybeShowWhatsNew({ currentVersion, lastSeenVersion, onDismiss })
|
|
9
|
+
* - Demo / screenshots: add ?whatsnew=1 to the URL (search OR hash query) and
|
|
10
|
+
* this file self-renders the modal over whatever view is loaded.
|
|
11
|
+
*
|
|
12
|
+
* Reuses the existing overlay pattern (see ui/dialog.js) and the design tokens
|
|
13
|
+
* in ui/styles.css. No new visual language, no one-off values.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// HTML-escape helper — guard for load order (dialog.js usually defines `esc`).
|
|
17
|
+
if (typeof esc === "undefined") {
|
|
18
|
+
// eslint-disable-next-line no-var
|
|
19
|
+
var _wnEscMap = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
20
|
+
// eslint-disable-next-line no-var
|
|
21
|
+
var esc = function (str) {
|
|
22
|
+
return String(str).replace(/[&<>"']/g, function (c) { return _wnEscMap[c]; });
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Release content. In production this is sourced from the current CHANGELOG.md
|
|
28
|
+
* section (CTO follow-up). For the scaffold it's the real, condensed 1.7.x story
|
|
29
|
+
* (the 1.7.1–1.7.7 entries shipped together on 2026-06-27 — checkpoints + softened
|
|
30
|
+
* barge-in). Copy run through the humanify standard: verbs lead, no buzzwords.
|
|
31
|
+
*/
|
|
32
|
+
const WHATS_NEW = {
|
|
33
|
+
version: "1.7.7",
|
|
34
|
+
date: "June 27, 2026",
|
|
35
|
+
changelogUrl: "https://github.com/borismichel/vibespot/blob/main/CHANGELOG.md",
|
|
36
|
+
highlights: [
|
|
37
|
+
{
|
|
38
|
+
title: "Checkpoints survive interruptions",
|
|
39
|
+
body: "Refresh the tab, lock your iPad, or restart the server. An open design, structure, or brand checkpoint comes back exactly where you left it instead of expiring.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: "Mid-build messages wait their turn",
|
|
43
|
+
body: "Type while a page is generating and your message queues as “runs next” instead of cancelling the build. Hit Interrupt now when you want to change course.",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: "Every decision is on the record",
|
|
47
|
+
body: "Approving a checkpoint leaves a line in the chat: the palette and fonts you chose, the sections you kept. You can see what you signed off on.",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Four-pointed brand star (same mark as the topbar logo), accent-gradient fill.
|
|
53
|
+
const _WN_STAR = `
|
|
54
|
+
<svg viewBox="0 0 512 512" width="22" height="22" aria-hidden="true">
|
|
55
|
+
<defs>
|
|
56
|
+
<linearGradient id="wn-star-grad" x1="0" y1="0" x2="1" y2="1">
|
|
57
|
+
<stop offset="0" stop-color="#e8613a"/>
|
|
58
|
+
<stop offset="1" stop-color="#f2825f"/>
|
|
59
|
+
</linearGradient>
|
|
60
|
+
</defs>
|
|
61
|
+
<path d="M256 76 Q280 220 436 256 Q280 292 256 436 Q232 292 76 256 Q232 220 256 76 Z" fill="url(#wn-star-grad)"/>
|
|
62
|
+
</svg>`;
|
|
63
|
+
|
|
64
|
+
let _wnOpen = false;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Render the "what's new" modal.
|
|
68
|
+
* @param {object} [opts]
|
|
69
|
+
* @param {object} [opts.content] — release content (defaults to WHATS_NEW)
|
|
70
|
+
* @param {Function} [opts.onDismiss] — called when the user dismisses the modal
|
|
71
|
+
* @returns {Promise<void>} resolves on dismiss
|
|
72
|
+
*/
|
|
73
|
+
function showWhatsNew(opts) {
|
|
74
|
+
opts = opts || {};
|
|
75
|
+
const content = opts.content || WHATS_NEW;
|
|
76
|
+
if (_wnOpen) return Promise.resolve();
|
|
77
|
+
_wnOpen = true;
|
|
78
|
+
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const lastFocused = document.activeElement;
|
|
81
|
+
|
|
82
|
+
const overlay = document.createElement("div");
|
|
83
|
+
overlay.className = "whatsnew-overlay";
|
|
84
|
+
|
|
85
|
+
const bullets = content.highlights.map((h) => `
|
|
86
|
+
<li class="whatsnew__item">
|
|
87
|
+
<span class="whatsnew__marker" aria-hidden="true">${_WN_STAR}</span>
|
|
88
|
+
<div class="whatsnew__item-text">
|
|
89
|
+
<span class="whatsnew__item-title">${esc(h.title)}</span>
|
|
90
|
+
<span class="whatsnew__item-body">${esc(h.body)}</span>
|
|
91
|
+
</div>
|
|
92
|
+
</li>`).join("");
|
|
93
|
+
|
|
94
|
+
overlay.innerHTML = `
|
|
95
|
+
<div class="whatsnew-dialog" role="dialog" aria-modal="true"
|
|
96
|
+
aria-labelledby="whatsnew-title" aria-describedby="whatsnew-desc">
|
|
97
|
+
<button class="whatsnew__close" type="button" data-action="dismiss" aria-label="Close">
|
|
98
|
+
<svg class="icon icon--sm" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
99
|
+
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
100
|
+
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
101
|
+
</svg>
|
|
102
|
+
</button>
|
|
103
|
+
|
|
104
|
+
<div class="whatsnew__brand">
|
|
105
|
+
<span class="whatsnew__logo">${_WN_STAR}</span>
|
|
106
|
+
<span class="whatsnew__wordmark">vibeSpot</span>
|
|
107
|
+
<span class="whatsnew__version">v${esc(content.version)}</span>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<h2 class="whatsnew__title" id="whatsnew-title">What’s new in vibeSpot ${esc(content.version)}</h2>
|
|
111
|
+
<p class="whatsnew__date" id="whatsnew-desc">Released ${esc(content.date)}</p>
|
|
112
|
+
|
|
113
|
+
<ul class="whatsnew__list">${bullets}</ul>
|
|
114
|
+
|
|
115
|
+
<div class="whatsnew__actions">
|
|
116
|
+
<button class="btn btn--secondary" type="button" data-action="dismiss">Got it</button>
|
|
117
|
+
<a class="btn btn--primary" href="${esc(content.changelogUrl)}"
|
|
118
|
+
target="_blank" rel="noopener noreferrer" data-action="changelog">
|
|
119
|
+
Read the full changelog
|
|
120
|
+
</a>
|
|
121
|
+
</div>
|
|
122
|
+
</div>`;
|
|
123
|
+
|
|
124
|
+
document.body.appendChild(overlay);
|
|
125
|
+
|
|
126
|
+
const dialog = overlay.querySelector(".whatsnew-dialog");
|
|
127
|
+
const focusables = dialog.querySelectorAll('button, a[href], [tabindex]:not([tabindex="-1"])');
|
|
128
|
+
|
|
129
|
+
const close = () => {
|
|
130
|
+
if (!_wnOpen) return;
|
|
131
|
+
_wnOpen = false;
|
|
132
|
+
document.removeEventListener("keydown", onKey);
|
|
133
|
+
overlay.remove();
|
|
134
|
+
if (typeof opts.onDismiss === "function") opts.onDismiss();
|
|
135
|
+
if (lastFocused && typeof lastFocused.focus === "function") lastFocused.focus();
|
|
136
|
+
resolve();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Dismiss affordances: ✕, "Got it", backdrop click. The changelog link
|
|
140
|
+
// opens in a new tab and also counts as "seen" so it never nags again.
|
|
141
|
+
overlay.addEventListener("click", (e) => {
|
|
142
|
+
const action = e.target.closest("[data-action]");
|
|
143
|
+
if (e.target === overlay) return close(); // backdrop
|
|
144
|
+
if (!action) return;
|
|
145
|
+
if (action.dataset.action === "changelog") { close(); return; } // let the link open
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
close();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
function onKey(e) {
|
|
151
|
+
if (e.key === "Escape") { e.preventDefault(); return close(); }
|
|
152
|
+
if (e.key === "Tab" && focusables.length) { // simple focus trap
|
|
153
|
+
const first = focusables[0];
|
|
154
|
+
const last = focusables[focusables.length - 1];
|
|
155
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
156
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
document.addEventListener("keydown", onKey);
|
|
160
|
+
|
|
161
|
+
// Focus the primary action so keyboard users land inside the dialog.
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
const primary = dialog.querySelector('[data-action="changelog"]');
|
|
164
|
+
if (primary) primary.focus();
|
|
165
|
+
}, 50);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Production seam (CTO follow-up wires this on web-UI load).
|
|
171
|
+
* Shows the modal once per release: when the installed version differs from the
|
|
172
|
+
* version this user last dismissed. On dismiss, persist lastSeenVersion.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} args
|
|
175
|
+
* @param {string} args.currentVersion — package.json version
|
|
176
|
+
* @param {string} [args.lastSeenVersion]— from ~/.vibespot/config.json
|
|
177
|
+
* @param {Function} [args.onDismiss] — persist callback (write lastSeenVersion)
|
|
178
|
+
* @param {object} [args.content] — release content from CHANGELOG.md
|
|
179
|
+
* @returns {boolean} whether the modal was shown
|
|
180
|
+
*/
|
|
181
|
+
function maybeShowWhatsNew(args) {
|
|
182
|
+
args = args || {};
|
|
183
|
+
if (!args.currentVersion) return false;
|
|
184
|
+
if (args.currentVersion === args.lastSeenVersion) return false;
|
|
185
|
+
showWhatsNew({
|
|
186
|
+
content: args.content,
|
|
187
|
+
onDismiss: () => {
|
|
188
|
+
if (typeof args.onDismiss === "function") args.onDismiss(args.currentVersion);
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- Demo trigger (scaffold only) ------------------------------------------
|
|
195
|
+
// ?whatsnew=1 in the query string OR after the hash (e.g. #/app/foo?whatsnew=1)
|
|
196
|
+
// renders the modal so it can be captured standalone for screenshots.
|
|
197
|
+
function _wnDemoRequested() {
|
|
198
|
+
const inSearch = new URLSearchParams(window.location.search).get("whatsnew") === "1";
|
|
199
|
+
const hash = window.location.hash || "";
|
|
200
|
+
const qIndex = hash.indexOf("?");
|
|
201
|
+
const inHash = qIndex !== -1 &&
|
|
202
|
+
new URLSearchParams(hash.slice(qIndex + 1)).get("whatsnew") === "1";
|
|
203
|
+
return inSearch || inHash;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function _wnInitDemo() {
|
|
207
|
+
if (_wnDemoRequested()) {
|
|
208
|
+
// Small delay so the builder chrome paints first (modal sits over it).
|
|
209
|
+
setTimeout(() => showWhatsNew(), 400);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- Production bootstrap (VIB-1885) ----------------------------------------
|
|
214
|
+
// On launch, ask the server whether there's an undismissed release to show.
|
|
215
|
+
// The server reads assets/whats-new.json (generated from CHANGELOG.md) and
|
|
216
|
+
// gates on config.lastSeenVersion, so this surfaces once per upgrade. Dismissing
|
|
217
|
+
// persists lastSeenVersion via POST /api/whats-new/dismiss so it never nags again.
|
|
218
|
+
function _wnInitProd() {
|
|
219
|
+
if (_wnDemoRequested()) return; // demo path owns the modal
|
|
220
|
+
fetch("/api/whats-new")
|
|
221
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
222
|
+
.then((data) => {
|
|
223
|
+
if (!data || !data.show || !data.content) return;
|
|
224
|
+
maybeShowWhatsNew({
|
|
225
|
+
currentVersion: data.content.version,
|
|
226
|
+
lastSeenVersion: null, // server already decided show=true
|
|
227
|
+
content: data.content,
|
|
228
|
+
onDismiss: (version) => {
|
|
229
|
+
fetch("/api/whats-new/dismiss", {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: { "Content-Type": "application/json" },
|
|
232
|
+
body: JSON.stringify({ version }),
|
|
233
|
+
}).catch(() => {});
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
})
|
|
237
|
+
.catch(() => {}); // network/offline: silently skip — never block the app
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _wnInit() {
|
|
241
|
+
_wnInitDemo();
|
|
242
|
+
_wnInitProd();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (document.readyState === "loading") {
|
|
246
|
+
document.addEventListener("DOMContentLoaded", _wnInit);
|
|
247
|
+
} else {
|
|
248
|
+
_wnInit();
|
|
249
|
+
}
|
|
Binary file
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 400" width="1600" height="400">
|
|
2
|
-
<defs>
|
|
3
|
-
<style>
|
|
4
|
-
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400&family=Space+Grotesk:wght@700&display=swap');
|
|
5
|
-
</style>
|
|
6
|
-
<linearGradient id="star-grad" x1="0" y1="0" x2="1" y2="1">
|
|
7
|
-
<stop offset="0%" stop-color="#e8613a"/>
|
|
8
|
-
<stop offset="100%" stop-color="#f2825f"/>
|
|
9
|
-
</linearGradient>
|
|
10
|
-
<linearGradient id="bar-grad" x1="0" y1="0" x2="1" y2="0">
|
|
11
|
-
<stop offset="0%" stop-color="#e8613a"/>
|
|
12
|
-
<stop offset="100%" stop-color="#f2825f"/>
|
|
13
|
-
</linearGradient>
|
|
14
|
-
<radialGradient id="glow1" cx="75%" cy="10%" r="45%">
|
|
15
|
-
<stop offset="0%" stop-color="#e8613a" stop-opacity="0.08"/>
|
|
16
|
-
<stop offset="40%" stop-color="#e8613a" stop-opacity="0.02"/>
|
|
17
|
-
<stop offset="65%" stop-color="#e8613a" stop-opacity="0"/>
|
|
18
|
-
</radialGradient>
|
|
19
|
-
<radialGradient id="glow2" cx="20%" cy="95%" r="35%">
|
|
20
|
-
<stop offset="0%" stop-color="#e8613a" stop-opacity="0.06"/>
|
|
21
|
-
<stop offset="40%" stop-color="#e8613a" stop-opacity="0.01"/>
|
|
22
|
-
<stop offset="65%" stop-color="#e8613a" stop-opacity="0"/>
|
|
23
|
-
</radialGradient>
|
|
24
|
-
</defs>
|
|
25
|
-
|
|
26
|
-
<!-- Background -->
|
|
27
|
-
<rect width="1600" height="400" fill="#faf9f7"/>
|
|
28
|
-
<rect width="1600" height="400" fill="url(#glow1)"/>
|
|
29
|
-
<rect width="1600" height="400" fill="url(#glow2)"/>
|
|
30
|
-
|
|
31
|
-
<!-- Decorative stars -->
|
|
32
|
-
<g opacity="0.10"><path d="M214 69 C214.6 78.6,221.4 85.4,231 86 C221.4 86.6,214.6 93.4,214 103 C213.4 93.4,206.6 86.6,197 86 C206.6 85.4,213.4 78.6,214 69Z" fill="#e8613a"/></g>
|
|
33
|
-
<g opacity="0.07"><path d="M149 295 C149.4 301.4,153.6 305.6,160 306 C153.6 306.4,149.4 310.6,149 317 C148.6 310.6,144.4 306.4,138 306 C144.4 305.6,148.6 301.4,149 295Z" fill="#e8613a"/></g>
|
|
34
|
-
<g opacity="0.09"><path d="M1372 88 C1372.6 97,1378.6 103,1388 103.6 C1378.6 104.2,1372.6 110.2,1372 119.2 C1371.4 110.2,1365.4 104.2,1356 103.6 C1365.4 103,1371.4 97,1372 88Z" fill="#e8613a"/></g>
|
|
35
|
-
<g opacity="0.06"><path d="M1428 325 C1428.8 337,1437 345.2,1449 346 C1437 346.8,1428.8 355,1428 367 C1427.2 355,1419 346.8,1407 346 C1419 345.2,1427.2 337,1428 325Z" fill="#e8613a"/></g>
|
|
36
|
-
<g opacity="0.05"><path d="M371 175 C371.3 179.8,374.2 182.7,379 183 C374.2 183.3,371.3 186.2,371 191 C370.7 186.2,367.8 183.3,363 183 C367.8 182.7,370.7 179.8,371 175Z" fill="#e8613a"/></g>
|
|
37
|
-
<g opacity="0.07"><path d="M413 303 C413.4 310,418 314.6,425 315 C418 315.4,413.4 320,413 327 C412.6 320,408 315.4,401 315 C408 314.6,412.6 310,413 303Z" fill="#e8613a"/></g>
|
|
38
|
-
<g opacity="0.06"><path d="M1188 57 C1188.3 62.4,1191.6 65.7,1197 66 C1191.6 66.3,1188.3 69.6,1188 75 C1187.7 69.6,1184.4 66.3,1179 66 C1184.4 65.7,1187.7 62.4,1188 57Z" fill="#e8613a"/></g>
|
|
39
|
-
<g opacity="0.05"><path d="M1172 340 C1172.5 348,1177.8 353.3,1186 353.8 C1177.8 354.3,1172.5 359.6,1172 367.6 C1171.5 359.6,1166.2 354.3,1158 353.8 C1166.2 353.3,1171.5 348,1172 340Z" fill="#e8613a"/></g>
|
|
40
|
-
<g opacity="0.04"><path d="M1288 326 C1288.2 330,1290.6 332.4,1295 332.6 C1290.6 332.8,1288.2 335.2,1288 339.2 C1287.8 335.2,1285.4 332.8,1281 332.6 C1285.4 332.4,1287.8 330,1288 326Z" fill="#e8613a"/></g>
|
|
41
|
-
<g opacity="0.04"><path d="M533 138 C533.2 141.6,535.4 143.8,539 144 C535.4 144.2,533.2 146.4,533 150 C532.8 146.4,530.6 144.2,527 144 C530.6 143.8,532.8 141.6,533 138Z" fill="#e8613a"/></g>
|
|
42
|
-
|
|
43
|
-
<!-- Logo mark (dark container + star) -->
|
|
44
|
-
<g transform="translate(680, 128)">
|
|
45
|
-
<rect width="80" height="80" rx="18" fill="#0c0a09"/>
|
|
46
|
-
<g transform="translate(15, 15) scale(0.0977)">
|
|
47
|
-
<path d="M256 28 C260 140, 372 252, 484 256 C372 260, 260 372, 256 484 C252 372, 140 260, 28 256 C140 252, 252 140, 256 28Z" fill="url(#star-grad)"/>
|
|
48
|
-
</g>
|
|
49
|
-
</g>
|
|
50
|
-
|
|
51
|
-
<!-- Wordmark -->
|
|
52
|
-
<text x="784" y="183" font-family="'Space Grotesk', system-ui, -apple-system, sans-serif" font-weight="700" font-size="60" fill="#1a1715" letter-spacing="-1.8">vibeSpot</text>
|
|
53
|
-
|
|
54
|
-
<!-- Tagline -->
|
|
55
|
-
<text x="800" y="234" font-family="'DM Sans', system-ui, -apple-system, sans-serif" font-weight="400" font-size="21" fill="#78716c" text-anchor="middle" letter-spacing="0.2">Build HubSpot landing pages with AI</text>
|
|
56
|
-
|
|
57
|
-
<!-- Accent bar -->
|
|
58
|
-
<rect x="776" y="256" width="48" height="3" rx="1.5" fill="url(#bar-grad)" opacity="0.6"/>
|
|
59
|
-
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|