feat(raw_apps): surface UI Builder build errors over the preview pane (#9316)

* feat(raw_apps): surface UI Builder build errors over the preview pane

Companion to the matching change in the UI Builder repo (see linked PR),
which stops rendering the build-error overlay over the VS Code editor
iframe and instead emits a `buildError` postMessage on every build
(message: undefined on success to clear).

Listen for that message on the existing window message handler (already
source-gated by the UI Builder iframe), store it in a `buildError`
$state, and surface it in two places:

* A red banner over the preview iframe, sibling to the existing logs
  overlay (`top-12 left-2 right-2 z-20` so it clears the tab bar) —
  failures appear right where the user looks for the rendered output.
* The Preview tab's icon and label tint red
  (`text-red-600 dark:text-red-400`, matching the existing error
  convention in raw_apps) — important in single-tab mode where the
  preview pane is collapsed to 0px and the banner would be hidden.
  Done by mapping `leftPaneTabs` / `rightPaneTabs` through a small
  `tintPreviewOnError` helper so the source-of-truth `tabs` array is
  untouched (DnD, ordering, fallback selection keep using the original
  previewTab object).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(raw_apps): use Alert component for the build-error banner

Replace the hand-rolled red div with the shared `Alert` component
(`type="error"`, `title="Build failed"`). The error text stays in a
`<pre>` child so multi-line bundler output keeps its formatting, with
`max-h-60` so a long error never takes over the whole preview pane.

The absolute-positioned wrapper (`top-12 left-2 right-2 z-20`) and the
`role="alert"` move to that wrapper so the Alert component itself stays
unstyled at the call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(raw_apps): solid bg-surface backing behind build-error Alert

The Alert's error background is semi-transparent in dark mode
(`bg-red-900/40` in `common/alert/model.ts`), so the preview iframe
shows through when the banner is laid over it. Add a `::before`
pseudo on the Alert root with `bg-surface` (matched `rounded-md`,
`-z-10` so it sits behind the red bg) to give it a solid plate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(raw_apps): isolate banner stacking context, DRY tab tint chain

Two small follow-ups from review:

* Add `isolate` to the build-error banner wrapper so the `before:-z-10`
  pseudo's stacking context is pinned locally — it works today because
  `position: absolute` + `z-20` creates one, but `isolate` makes the
  dependency self-documenting and survives a future refactor that
  removes the explicit `z-20`.
* Extract `tintTabs = (ts) => ts.map(tintPreviewOnError)` so the two
  `$derived` blocks for leftPaneTabs / rightPaneTabs read identically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(raw_apps): trim build-error overlay comments

Per review feedback. Keep only the load-bearing facts (bg-surface backs
the Alert's translucent red, isolate pins the pseudo stacking, the
`message: undefined` clear convention) and drop the prose context that
duplicated what the code already shows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(raw_apps): bump bundled ui_builder to 00c9834

Brings in the postMessage emission from
windmill-labs/windmill-code-ui-builder#9 (merged) so this PR's host
listener actually receives `buildError` events. SHA verified against
the R2 artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-06-08 11:35:46 +02:00
committed by tristantr
co-authored by Claude Opus 4.7
parent 106e77e8d2
commit 45bae59e9c
2 changed files with 35 additions and 4 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
{
"baseUrl": "https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev",
"version": "b4f6219",
"sha256": "69575342aab968fc32a89d3410c21bf888b0f00ce5c68fe80936d3adc1359b36"
"version": "00c9834",
"sha256": "5757e5b9cbf79c20d507dc4c588640368e84b873cb48f3806ca8c67fd1aa625f"
}
@@ -6,6 +6,7 @@
import RawAppEditorHeader from './RawAppEditorHeader.svelte'
import RawAppYamlEditor, { type RawAppYamlUpdate } from './RawAppYamlEditor.svelte'
import type Drawer from '../common/drawer/Drawer.svelte'
import Alert from '../common/alert/Alert.svelte'
import { type Policy, WorkspaceService } from '$lib/gen'
import DiffDrawer from '../DiffDrawer.svelte'
import { deepEqual } from 'fast-equals'
@@ -195,6 +196,9 @@
// them as an overlay inside the preview pane (right side) so they're
// visually tied to the build output, not to the source editor.
let logs = $state('')
// Latest UI Builder error; cleared on next successful build.
let buildError = $state<string | undefined>(undefined)
let logsCollapsed = $state(false)
let logsDiv: HTMLDivElement | undefined = $state(undefined)
$effect(() => {
@@ -229,13 +233,20 @@
? 'file'
: 'runnable'
)
// Tint the Preview tab red so the error is visible when Preview isn't active.
function tintPreviewOnError(t: TabItem): TabItem {
if (t.id !== PREVIEW_TAB_ID || !buildError) return t
const errClass = 'text-red-600 dark:text-red-400'
return { ...t, iconClass: errClass, labelClass: errClass }
}
const tintTabs = (ts: TabItem[]) => ts.map(tintPreviewOnError)
// Single mode: both bars mirror the full list (the visible pane carries
// every tab). Split mode: left = files/runnables, right = Preview only.
const leftPaneTabs = $derived<TabItem[]>(
splitWithPreview ? tabs.filter((t) => t.id !== PREVIEW_TAB_ID) : tabs
tintTabs(splitWithPreview ? tabs.filter((t) => t.id !== PREVIEW_TAB_ID) : tabs)
)
const rightPaneTabs = $derived<TabItem[]>(
splitWithPreview ? tabs.filter((t) => t.id === PREVIEW_TAB_ID) : tabs
tintTabs(splitWithPreview ? tabs.filter((t) => t.id === PREVIEW_TAB_ID) : tabs)
)
// In split mode the right bar always highlights Preview, regardless of the
// left pane's active file/runnable.
@@ -938,6 +949,12 @@
return
}
// `message: undefined` arrives on the next successful build and clears the banner.
if (fromUiBuilder && e.data.type === 'buildError') {
buildError = typeof e.data.message === 'string' ? e.data.message : undefined
return
}
// Inspector events come exclusively from the preview iframe.
if (fromPreview && e.data.type === 'inspectorSelect') {
inspectorElement = e.data.element as InspectorElementInfo
@@ -1557,6 +1574,20 @@
src="/ui_builder/app-preview.html"
class="w-full flex-1 block"
></iframe>
{#if buildError}
<!-- top-12 clears the tab bar; `before:bg-surface` backs the
Alert's translucent red; `isolate` pins the pseudo's stacking context. -->
<div class="absolute top-12 left-2 right-2 z-20 isolate" role="alert">
<Alert
type="error"
title="Build failed"
class="relative before:absolute before:inset-0 before:-z-10 before:rounded-md before:bg-surface before:content-['']"
>
<pre
class="overflow-auto whitespace-pre-wrap text-xs max-h-60">{buildError}</pre>
</Alert>
</div>
{/if}
{#if logs}
<div
class="absolute right-0 bottom-0 z-20 max-w-[500px] w-full flex flex-col text-xs p-1 border border-border-light rounded-tl-md bg-surface text-primary {logsCollapsed