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.
Files changed (40) hide show
  1. package/README.md +24 -3
  2. package/assets/whats-new.json +27 -0
  3. package/dist/index.js +579 -681
  4. package/dist/index.js.map +1 -1
  5. package/package.json +8 -5
  6. package/ui/chat.js +671 -9
  7. package/ui/dashboard.js +53 -12
  8. package/ui/docs/index.html +57 -2
  9. package/ui/email-preview.js +1 -5
  10. package/ui/escape-html.js +14 -0
  11. package/ui/field-editor.js +6 -12
  12. package/ui/field-save.js +82 -0
  13. package/ui/index.html +10 -4
  14. package/ui/inline-edit.js +116 -570
  15. package/ui/plan.js +22 -13
  16. package/ui/preview-agent.js +1050 -0
  17. package/ui/preview.js +248 -265
  18. package/ui/section-controls.js +16 -622
  19. package/ui/setup.js +73 -20
  20. package/ui/styles.css +424 -0
  21. package/ui/upload-panel.js +7 -8
  22. package/ui/whats-new.js +249 -0
  23. package/assets/readme/00-hero-banner.png +0 -0
  24. package/assets/readme/00-hero-banner.svg +0 -59
  25. package/assets/readme/01-vibe-coding-hero.png +0 -0
  26. package/assets/readme/02-plan-mode.png +0 -0
  27. package/assets/readme/03-figma-import.png +0 -0
  28. package/assets/readme/04-multi-page-sites.png +0 -0
  29. package/assets/readme/05-inline-wysiwyg.png +0 -0
  30. package/assets/readme/06-hubspot-upload.png +0 -0
  31. package/ui/docs/screenshots/brand-kit-preview.png +0 -0
  32. package/ui/docs/screenshots/content-type-dropdown.png +0 -0
  33. package/ui/docs/screenshots/editor-full-layout.png +0 -0
  34. package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
  35. package/ui/docs/screenshots/module-overview-slideout.png +0 -0
  36. package/ui/docs/screenshots/multi-page-tree.png +0 -0
  37. package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
  38. package/ui/docs/screenshots/split-pane-view.png +0 -0
  39. package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
  40. 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
- await fetch("/api/settings/engine", {
143
- method: "POST",
144
- headers: { "Content-Type": "application/json" },
145
- body: JSON.stringify({ engine }),
146
- });
147
- info.activeEngine = engine;
148
- info.aiAvailable = true;
149
- // Update statusbar
150
- const statusEngine = document.getElementById("status-engine");
151
- if (statusEngine) statusEngine.textContent = ENGINE_DISPLAY_NAMES[engine] || engine;
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 pollJob(data.jobId);
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
- async function pollJob(jobId) {
1083
- for (let i = 0; i < 60; i++) {
1084
- await new Promise((r) => setTimeout(r, 2000));
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
- const res = await fetch("/api/settings/job/" + jobId);
1087
- const data = await res.json();
1088
- if (data.status === "completed" || data.status === "failed") return;
1089
- } catch { return; }
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
@@ -8216,3 +8216,427 @@ body { display: flex; }
8216
8216
  display: none;
8217
8217
  }
8218
8218
  }
8219
+
8220
+ /* ---------------------------------------------------------------------------
8221
+ Checkpoint gate card (VIB-1877) — design preview before the module build.
8222
+ --------------------------------------------------------------------------- */
8223
+ .checkpoint {
8224
+ border: 1px solid var(--border-hover);
8225
+ border-radius: var(--radius);
8226
+ background: var(--bg-elevated);
8227
+ padding: var(--space-4);
8228
+ display: flex;
8229
+ flex-direction: column;
8230
+ gap: var(--space-3);
8231
+ max-width: 520px;
8232
+ }
8233
+ .checkpoint__head {
8234
+ display: flex;
8235
+ align-items: center;
8236
+ gap: var(--space-2);
8237
+ flex-wrap: wrap;
8238
+ }
8239
+ .checkpoint__badge {
8240
+ font-size: var(--text-xs);
8241
+ font-weight: 600;
8242
+ letter-spacing: 0.08em;
8243
+ text-transform: uppercase;
8244
+ color: var(--accent);
8245
+ background: var(--accent-dim);
8246
+ border-radius: var(--radius-pill);
8247
+ padding: 3px 9px;
8248
+ }
8249
+ .checkpoint__headline {
8250
+ font-family: var(--font-display);
8251
+ font-size: var(--text-md);
8252
+ color: var(--text);
8253
+ }
8254
+ .checkpoint__hero {
8255
+ border-radius: var(--radius-sm);
8256
+ overflow: hidden;
8257
+ border: 1px solid var(--border);
8258
+ background: #000;
8259
+ }
8260
+ .checkpoint__hero-frame {
8261
+ display: block;
8262
+ width: 100%;
8263
+ height: 230px;
8264
+ border: 0;
8265
+ }
8266
+ .checkpoint__tokens {
8267
+ display: flex;
8268
+ flex-wrap: wrap;
8269
+ gap: var(--space-4);
8270
+ align-items: flex-start;
8271
+ }
8272
+ .checkpoint__palette {
8273
+ display: flex;
8274
+ flex-wrap: wrap;
8275
+ gap: var(--space-2);
8276
+ flex: 1 1 220px;
8277
+ }
8278
+ .checkpoint__swatch {
8279
+ display: flex;
8280
+ align-items: center;
8281
+ gap: 6px;
8282
+ font-size: var(--text-sm);
8283
+ color: var(--text-dim);
8284
+ }
8285
+ .checkpoint__swatch-chip {
8286
+ width: 16px;
8287
+ height: 16px;
8288
+ border-radius: 4px;
8289
+ border: 1px solid var(--border-hover);
8290
+ display: inline-block;
8291
+ }
8292
+ .checkpoint__type {
8293
+ display: flex;
8294
+ flex-direction: column;
8295
+ gap: 2px;
8296
+ }
8297
+ .checkpoint__type-row {
8298
+ font-size: var(--text-lg);
8299
+ color: var(--text);
8300
+ line-height: 1.2;
8301
+ }
8302
+ .checkpoint__type-row--body {
8303
+ font-size: var(--text-base);
8304
+ color: var(--text-dim);
8305
+ }
8306
+ .checkpoint__muted { color: var(--text-dim); font-size: var(--text-sm); }
8307
+ .checkpoint__meta { font-size: var(--text-sm); color: var(--text-dim); }
8308
+ .checkpoint__cost { color: var(--text-dim); }
8309
+ .checkpoint__steer-input {
8310
+ width: 100%;
8311
+ resize: vertical;
8312
+ background: var(--bg-input);
8313
+ border: 1px solid var(--border-hover);
8314
+ border-radius: var(--radius-sm);
8315
+ color: var(--text);
8316
+ font-family: var(--font);
8317
+ font-size: var(--text-base);
8318
+ padding: var(--space-2);
8319
+ }
8320
+ .checkpoint__actions {
8321
+ display: flex;
8322
+ flex-wrap: wrap;
8323
+ gap: var(--space-2);
8324
+ }
8325
+ .checkpoint__btn { font-size: var(--text-base); }
8326
+ .checkpoint__btn:disabled { opacity: 0.5; cursor: default; }
8327
+
8328
+ /* Structure checkpoint (VIB-1879) — editable module outline. */
8329
+ .checkpoint__narrative {
8330
+ font-size: var(--text-sm);
8331
+ color: var(--text-dim);
8332
+ line-height: 1.5;
8333
+ }
8334
+ .checkpoint__outline {
8335
+ display: flex;
8336
+ flex-direction: column;
8337
+ gap: var(--space-2);
8338
+ }
8339
+ .checkpoint__row {
8340
+ display: flex;
8341
+ align-items: center;
8342
+ gap: var(--space-2);
8343
+ background: var(--bg-input);
8344
+ border: 1px solid var(--border);
8345
+ border-radius: var(--radius-sm);
8346
+ padding: 6px 8px;
8347
+ }
8348
+ .checkpoint__row-move {
8349
+ display: flex;
8350
+ flex-direction: column;
8351
+ gap: 1px;
8352
+ }
8353
+ .checkpoint__row-btn {
8354
+ border: 0;
8355
+ background: transparent;
8356
+ color: var(--text-dim);
8357
+ cursor: pointer;
8358
+ line-height: 1;
8359
+ font-size: 9px;
8360
+ padding: 1px 2px;
8361
+ border-radius: 3px;
8362
+ }
8363
+ .checkpoint__row-btn:hover { color: var(--text); background: var(--bg-elevated); }
8364
+ .checkpoint__row-main {
8365
+ display: flex;
8366
+ flex-direction: column;
8367
+ gap: 1px;
8368
+ flex: 1 1 auto;
8369
+ min-width: 0;
8370
+ }
8371
+ .checkpoint__row-name {
8372
+ background: transparent;
8373
+ border: 1px solid transparent;
8374
+ border-radius: var(--radius-sm);
8375
+ color: var(--text);
8376
+ font-family: var(--font-mono, monospace);
8377
+ font-size: var(--text-base);
8378
+ padding: 2px 4px;
8379
+ width: 100%;
8380
+ }
8381
+ .checkpoint__row-name:hover { border-color: var(--border-hover); }
8382
+ .checkpoint__row-name:focus {
8383
+ outline: none;
8384
+ border-color: var(--accent);
8385
+ background: var(--bg);
8386
+ }
8387
+ .checkpoint__row-desc {
8388
+ font-size: var(--text-xs);
8389
+ color: var(--text-dim);
8390
+ padding: 0 4px;
8391
+ overflow: hidden;
8392
+ text-overflow: ellipsis;
8393
+ white-space: nowrap;
8394
+ }
8395
+ .checkpoint__row-remove {
8396
+ border: 0;
8397
+ background: transparent;
8398
+ color: var(--text-dim);
8399
+ cursor: pointer;
8400
+ font-size: var(--text-base);
8401
+ padding: 2px 6px;
8402
+ border-radius: var(--radius-sm);
8403
+ flex: 0 0 auto;
8404
+ }
8405
+ .checkpoint__row-remove:hover { color: var(--danger, #e5484d); background: var(--bg-elevated); }
8406
+ .checkpoint__add {
8407
+ align-self: flex-start;
8408
+ font-size: var(--text-sm);
8409
+ }
8410
+
8411
+ /* One-shot send button — slightly dimmer than the primary send. */
8412
+ .chat__send--oneshot {
8413
+ opacity: 0.7;
8414
+ }
8415
+ .chat__send--oneshot:hover:not(:disabled) {
8416
+ opacity: 1;
8417
+ }
8418
+
8419
+ /* Brand-intake gate (VIB-1878) — front-of-flow ask-back card. */
8420
+ .brand-intake__choices {
8421
+ display: flex;
8422
+ flex-direction: column;
8423
+ gap: var(--space-2);
8424
+ margin: var(--space-3) 0;
8425
+ }
8426
+ .brand-intake__choice {
8427
+ display: flex;
8428
+ flex-direction: column;
8429
+ align-items: flex-start;
8430
+ gap: 2px;
8431
+ text-align: left;
8432
+ padding: var(--space-3);
8433
+ height: auto;
8434
+ }
8435
+ .brand-intake__choice-title { font-weight: 600; }
8436
+ .brand-intake__choice-hint { font-size: var(--text-sm); opacity: 0.75; }
8437
+ .brand-intake__form {
8438
+ display: flex;
8439
+ flex-direction: column;
8440
+ gap: var(--space-2);
8441
+ margin: var(--space-3) 0;
8442
+ }
8443
+ .brand-intake__field {
8444
+ display: flex;
8445
+ flex-direction: column;
8446
+ gap: 4px;
8447
+ }
8448
+ .brand-intake__label { font-size: var(--text-sm); font-weight: 600; }
8449
+ .brand-intake__input {
8450
+ width: 100%;
8451
+ font-family: inherit;
8452
+ font-size: var(--text-base);
8453
+ padding: var(--space-2);
8454
+ border: 1px solid var(--border);
8455
+ border-radius: var(--radius-sm);
8456
+ background: var(--surface);
8457
+ color: var(--text);
8458
+ resize: vertical;
8459
+ }
8460
+ .brand-intake__form-actions {
8461
+ display: flex;
8462
+ gap: var(--space-2);
8463
+ margin-top: var(--space-1);
8464
+ }
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
+ }
@@ -116,16 +116,15 @@ function showHubSpotSetupDialog(mode) {
116
116
  });
117
117
  const data = await res.json();
118
118
  if (data.jobId) {
119
- // Poll until done
120
- for (let i = 0; i < 60; i++) {
121
- await new Promise((r) => setTimeout(r, 2000));
122
- const jr = await fetch("/api/settings/job/" + data.jobId).then((r) => r.json());
123
- if (jr.status === "completed") { close(true); return; }
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
  });