vibespot 1.2.0 → 1.3.1

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 (49) hide show
  1. package/README.md +54 -5
  2. package/assets/blog-rules.md +251 -0
  3. package/assets/email-rules.md +390 -0
  4. package/assets/humanify-guide.md +300 -101
  5. package/assets/plan-templates/blog-content-hub.md +18 -9
  6. package/assets/plan-templates/email-announcement.md +41 -0
  7. package/assets/plan-templates/email-event-invite.md +43 -0
  8. package/assets/plan-templates/email-newsletter.md +41 -0
  9. package/assets/plan-templates/email-re-engagement.md +42 -0
  10. package/assets/plan-templates/email-welcome.md +41 -0
  11. package/dist/index.js +1460 -387
  12. package/dist/index.js.map +1 -1
  13. package/package.json +5 -5
  14. package/starters/06-blog-content-hub.json +75 -0
  15. package/starters/06-email-welcome.json +60 -0
  16. package/starters/07-email-announcement.json +60 -0
  17. package/starters/08-email-newsletter.json +52 -0
  18. package/ui/chat.js +777 -63
  19. package/ui/code-editor.js +49 -7
  20. package/ui/dashboard.js +379 -93
  21. package/ui/docs/docs.css +29 -0
  22. package/ui/docs/index.html +416 -119
  23. package/ui/docs/screenshots/asset-type-cards.png +0 -0
  24. package/ui/docs/screenshots/brand-kit-preview.png +0 -0
  25. package/ui/docs/screenshots/content-type-dropdown.png +0 -0
  26. package/ui/docs/screenshots/deploy-progress.png +0 -0
  27. package/ui/docs/screenshots/editor-full-layout.png +0 -0
  28. package/ui/docs/screenshots/email-client-preview.png +0 -0
  29. package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
  30. package/ui/docs/screenshots/module-overview-slideout.png +0 -0
  31. package/ui/docs/screenshots/multi-page-tree.png +0 -0
  32. package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
  33. package/ui/docs/screenshots/pipeline-progress.png +0 -0
  34. package/ui/docs/screenshots/project-overview-table.png +0 -0
  35. package/ui/docs/screenshots/split-pane-view.png +0 -0
  36. package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
  37. package/ui/docs/screenshots/workspace-tabs.png +0 -0
  38. package/ui/email-preview.js +109 -0
  39. package/ui/field-editor.js +72 -1
  40. package/ui/icons.js +120 -0
  41. package/ui/index.html +877 -629
  42. package/ui/inline-edit.js +710 -0
  43. package/ui/plan.js +0 -0
  44. package/ui/preview.js +101 -198
  45. package/ui/section-controls.js +628 -0
  46. package/ui/settings.js +58 -16
  47. package/ui/setup.js +750 -140
  48. package/ui/styles.css +3430 -952
  49. package/ui/upload-panel.js +47 -20
package/ui/preview.js CHANGED
@@ -3,12 +3,43 @@
3
3
  */
4
4
 
5
5
  const previewFrame = document.getElementById("preview-frame");
6
+ const previewEmptyState = document.getElementById("preview-empty-state");
6
7
 
7
8
  // Highlights to apply once the iframe finishes loading after the next refresh.
8
9
  let pendingChangedModules = null;
9
10
  let pendingNewModules = null;
10
11
 
12
+ /**
13
+ * Show or hide the preview empty state. Called when generation starts or when
14
+ * the iframe finishes loading and we can detect whether any modules rendered.
15
+ */
16
+ function setPreviewEmptyState(show) {
17
+ if (!previewEmptyState) return;
18
+ previewEmptyState.setAttribute("aria-hidden", show ? "false" : "true");
19
+ }
20
+
21
+ /**
22
+ * Inspect iframe contents post-load and toggle the empty state accordingly.
23
+ * Empty state stays visible if the rendered preview has no module content.
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
+ }
40
+
11
41
  previewFrame.addEventListener("load", () => {
42
+ syncEmptyStateFromFrame();
12
43
  if (!pendingChangedModules && !pendingNewModules) return;
13
44
  const changed = pendingChangedModules;
14
45
  const fresh = pendingNewModules;
@@ -151,6 +182,7 @@ function scrollPreviewToModule(moduleName) {
151
182
  * Called when AI generation starts to entertain the user while waiting.
152
183
  */
153
184
  function showGeneratingPreview() {
185
+ setPreviewEmptyState(false);
154
186
  const html = `<!DOCTYPE html>
155
187
  <html lang="en">
156
188
  <head>
@@ -381,215 +413,86 @@ function clearAllModulesWorking() {
381
413
  // Preview refresh is triggered by setup.js after a session is created.
382
414
  // Do NOT auto-refresh here.
383
415
 
416
+ // Select/edit mode has been unified into interact mode (inline-edit.js)
417
+
384
418
  // ---------------------------------------------------------------------------
385
- // Select modeclick an element in the preview to reference it in chat
419
+ // HubL validity badge aggregates per-module checks reported by chat.js into
420
+ // a single status pill in the preview toolbar.
386
421
  // ---------------------------------------------------------------------------
387
422
 
388
- const selectModeBtn = document.getElementById("select-mode-toggle");
389
- const previewContainer = document.getElementById("preview-container");
390
-
391
- let selectModeActive = false;
392
- let hoveredEl = null;
393
- let hoverLabelEl = null;
394
- let onMouseOver = null;
395
- let onMouseOut = null;
396
- let onClick = null;
397
- let onKeyDown = null;
398
-
399
- function ensureSelectModeStyles(doc) {
400
- if (doc.getElementById("vibespot-select-css")) return;
401
- const style = doc.createElement("style");
402
- style.id = "vibespot-select-css";
403
- style.textContent = `
404
- html.vibespot-select-mode, html.vibespot-select-mode * { cursor: crosshair !important; }
405
- html.vibespot-select-mode a, html.vibespot-select-mode button { pointer-events: none; }
406
- .vibespot-select-hover {
407
- outline: 2px solid #e8613a !important;
408
- outline-offset: 2px !important;
409
- background-color: rgba(232, 97, 58, 0.08) !important;
410
- }
411
- .vibespot-select-label {
412
- position: fixed;
413
- z-index: 2147483647;
414
- pointer-events: none;
415
- font: 500 11px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
416
- color: #fff;
417
- background: #e8613a;
418
- padding: 3px 8px;
419
- border-radius: 4px;
420
- box-shadow: 0 2px 6px rgba(0,0,0,0.25);
421
- white-space: nowrap;
422
- max-width: 70vw;
423
- overflow: hidden;
424
- text-overflow: ellipsis;
425
- }
426
- `;
427
- doc.head.appendChild(style);
428
- }
429
-
430
- function describeElement(el) {
431
- if (!el) return "";
432
- const moduleEl = el.closest("[data-module]");
433
- const moduleName = moduleEl ? moduleEl.getAttribute("data-module") : null;
434
- const tag = (el.tagName || "").toLowerCase();
435
- let kind = tag;
436
- if (tag.match(/^h[1-6]$/)) kind = "headline";
437
- else if (tag === "p") kind = "paragraph";
438
- else if (tag === "a") kind = "link";
439
- else if (tag === "button") kind = "button";
440
- else if (tag === "img") kind = "image";
441
- else if (tag === "ul" || tag === "ol") kind = "list";
442
- else if (tag === "li") kind = "list item";
443
-
444
- if (moduleName && moduleEl === el) return moduleName;
445
- if (moduleName) return `${moduleName} > ${kind}`;
446
- return kind;
447
- }
448
-
449
- function buildChatPrefill(el) {
450
- if (!el) return "";
451
- const moduleEl = el.closest("[data-module]");
452
- const moduleName = moduleEl ? moduleEl.getAttribute("data-module") : null;
453
- const tag = (el.tagName || "").toLowerCase();
454
- const text = (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 80);
455
-
456
- let elementPart;
457
- if (tag.match(/^h[1-6]$/)) elementPart = "the headline";
458
- else if (tag === "p") elementPart = "the paragraph";
459
- else if (tag === "a") elementPart = "the link";
460
- else if (tag === "button") elementPart = "the button";
461
- else if (tag === "img") elementPart = "the image";
462
- else elementPart = `the ${tag}`;
463
-
464
- if (moduleEl === el && moduleName) {
465
- return `In the ${moduleName} section, `;
466
- }
467
- if (moduleName) {
468
- const quote = text ? ` ("${text}${text.length === 80 ? "…" : ""}")` : "";
469
- return `In the ${moduleName} section, ${elementPart}${quote} `;
423
+ const hublBadgeEl = document.getElementById("hubl-badge");
424
+ const hublBadgeLabelEl = hublBadgeEl ? hublBadgeEl.querySelector(".hubl-badge__label") : null;
425
+ const hublBadgeCountEl = document.getElementById("hubl-badge-count");
426
+ const hublModuleIssues = new Map();
427
+ let hublBadgeReveal = null;
428
+
429
+ function applyHublBadgeState() {
430
+ if (!hublBadgeEl) return;
431
+ let totalIssues = 0;
432
+ for (const issues of hublModuleIssues.values()) totalIssues += issues.length;
433
+ const failedModules = Array.from(hublModuleIssues.values()).filter((arr) => arr.length > 0).length;
434
+
435
+ const state = totalIssues === 0 ? "valid" : "issues";
436
+ hublBadgeEl.dataset.state = state;
437
+ hublBadgeEl.classList.toggle("hubl-badge--valid", state === "valid");
438
+ hublBadgeEl.classList.toggle("hubl-badge--issues", state === "issues");
439
+
440
+ if (hublBadgeLabelEl) {
441
+ hublBadgeLabelEl.textContent = state === "valid" ? "Valid HubL" : "HubL issues";
470
442
  }
471
- return `${elementPart} `;
472
- }
473
-
474
- function clearHover(doc) {
475
- if (hoveredEl) {
476
- hoveredEl.classList.remove("vibespot-select-hover");
477
- hoveredEl = null;
478
- }
479
- if (hoverLabelEl && hoverLabelEl.parentNode) {
480
- hoverLabelEl.parentNode.removeChild(hoverLabelEl);
481
- }
482
- hoverLabelEl = null;
483
- }
484
-
485
- function attachSelectHandlers() {
486
- let doc;
487
- try {
488
- doc = previewFrame.contentDocument || previewFrame.contentWindow?.document;
489
- } catch { return; }
490
- if (!doc || !doc.body) return;
491
-
492
- ensureSelectModeStyles(doc);
493
- doc.documentElement.classList.add("vibespot-select-mode");
494
-
495
- onMouseOver = (e) => {
496
- const el = e.target;
497
- if (!el || el === doc.body || el === doc.documentElement) return;
498
- if (el.classList && el.classList.contains("vibespot-select-label")) return;
499
- if (hoveredEl === el) return;
500
- if (hoveredEl) hoveredEl.classList.remove("vibespot-select-hover");
501
- hoveredEl = el;
502
- el.classList.add("vibespot-select-hover");
503
-
504
- if (!hoverLabelEl) {
505
- hoverLabelEl = doc.createElement("div");
506
- hoverLabelEl.className = "vibespot-select-label";
507
- doc.body.appendChild(hoverLabelEl);
508
- }
509
- hoverLabelEl.textContent = describeElement(el);
510
- const rect = el.getBoundingClientRect();
511
- hoverLabelEl.style.top = Math.max(4, rect.top - 22) + "px";
512
- hoverLabelEl.style.left = Math.max(4, rect.left) + "px";
513
- };
514
-
515
- onMouseOut = (e) => {
516
- if (!e.relatedTarget) clearHover(doc);
517
- };
518
-
519
- onClick = (e) => {
520
- e.preventDefault();
521
- e.stopPropagation();
522
- const prefill = buildChatPrefill(e.target);
523
- deactivateSelectMode();
524
- if (typeof window.prefillChatInput === "function") {
525
- window.prefillChatInput(prefill);
443
+ if (hublBadgeCountEl) {
444
+ if (state === "valid") {
445
+ hublBadgeCountEl.classList.add("hidden");
446
+ hublBadgeCountEl.textContent = "";
447
+ } else {
448
+ hublBadgeCountEl.classList.remove("hidden");
449
+ hublBadgeCountEl.textContent = String(totalIssues);
526
450
  }
527
- };
528
-
529
- onKeyDown = (e) => {
530
- if (e.key === "Escape") deactivateSelectMode();
531
- };
532
-
533
- doc.addEventListener("mouseover", onMouseOver, true);
534
- doc.addEventListener("mouseout", onMouseOut, true);
535
- doc.addEventListener("click", onClick, true);
536
- doc.addEventListener("keydown", onKeyDown, true);
537
- }
538
-
539
- function detachSelectHandlers() {
540
- let doc;
541
- try {
542
- doc = previewFrame.contentDocument || previewFrame.contentWindow?.document;
543
- } catch { return; }
544
- if (!doc) return;
545
- doc.documentElement.classList.remove("vibespot-select-mode");
546
- clearHover(doc);
547
- if (onMouseOver) doc.removeEventListener("mouseover", onMouseOver, true);
548
- if (onMouseOut) doc.removeEventListener("mouseout", onMouseOut, true);
549
- if (onClick) doc.removeEventListener("click", onClick, true);
550
- if (onKeyDown) doc.removeEventListener("keydown", onKeyDown, true);
551
- onMouseOver = onMouseOut = onClick = onKeyDown = null;
552
- }
553
-
554
- function activateSelectMode() {
555
- if (selectModeActive) return;
556
- selectModeActive = true;
557
- if (selectModeBtn) selectModeBtn.setAttribute("aria-pressed", "true");
558
- if (previewContainer) previewContainer.classList.add("preview--select-mode");
559
- attachSelectHandlers();
560
- }
561
-
562
- function deactivateSelectMode() {
563
- if (!selectModeActive) return;
564
- selectModeActive = false;
565
- if (selectModeBtn) selectModeBtn.setAttribute("aria-pressed", "false");
566
- if (previewContainer) previewContainer.classList.remove("preview--select-mode");
567
- detachSelectHandlers();
568
- }
451
+ }
569
452
 
570
- function setSelectModeDisabled(disabled) {
571
- if (!selectModeBtn) return;
572
- if (disabled) {
573
- deactivateSelectMode();
574
- selectModeBtn.disabled = true;
453
+ if (state === "valid") {
454
+ const checked = hublModuleIssues.size;
455
+ hublBadgeEl.title = checked === 0
456
+ ? "HubL syntax check — no modules generated yet."
457
+ : `HubL syntax check — all ${checked} module${checked === 1 ? "" : "s"} parse cleanly.`;
575
458
  } else {
576
- selectModeBtn.disabled = false;
459
+ hublBadgeEl.title = `${totalIssues} HubL issue${totalIssues === 1 ? "" : "s"} across ${failedModules} module${failedModules === 1 ? "" : "s"}. Click to review.`;
577
460
  }
578
461
  }
579
462
 
580
- if (selectModeBtn) {
581
- selectModeBtn.addEventListener("click", () => {
582
- if (selectModeBtn.disabled) return;
583
- if (selectModeActive) deactivateSelectMode();
584
- else activateSelectMode();
585
- });
463
+ function flashHublBadge() {
464
+ if (!hublBadgeEl) return;
465
+ hublBadgeEl.classList.remove("hubl-badge--flash");
466
+ // Force reflow so the animation restarts.
467
+ void hublBadgeEl.offsetWidth;
468
+ hublBadgeEl.classList.add("hubl-badge--flash");
586
469
  }
587
470
 
588
- if (previewFrame) {
589
- previewFrame.addEventListener("load", () => {
590
- if (selectModeActive) attachSelectHandlers();
471
+ window.resetHublCheck = function () {
472
+ hublModuleIssues.clear();
473
+ applyHublBadgeState();
474
+ };
475
+
476
+ window.reportHublCheck = function (moduleName, issues) {
477
+ if (!moduleName) return;
478
+ hublModuleIssues.set(moduleName, Array.isArray(issues) ? issues : []);
479
+ applyHublBadgeState();
480
+ flashHublBadge();
481
+ };
482
+
483
+ if (hublBadgeEl) {
484
+ applyHublBadgeState();
485
+ hublBadgeEl.addEventListener("click", () => {
486
+ if (hublBadgeEl.dataset.state !== "issues") return;
487
+ const lines = [];
488
+ for (const [name, issues] of hublModuleIssues) {
489
+ if (!issues.length) continue;
490
+ lines.push(`• ${name}: ${issues.map((i) => i.message || i.kind).join(", ")}`);
491
+ }
492
+ if (typeof appendSystemMessage === "function") {
493
+ appendSystemMessage(`HubL issues in current run:\n${lines.join("\n")}`);
494
+ } else {
495
+ console.warn("HubL issues:\n" + lines.join("\n"));
496
+ }
591
497
  });
592
498
  }
593
-
594
- window.setSelectModeDisabled = setSelectModeDisabled;
595
- window.deactivateSelectMode = deactivateSelectMode;