mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 16:02:11 +00:00
fix: keep the mermaid fullscreen dialog in its pane and its emoji vector (#10541)
* fix: keep the mermaid fullscreen dialog inside its pane and its emoji vector Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: load only the fonts a diagram needs and size chrome per breakpoint Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: cover every emoji class in the font preload without restyling diagrams Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the dialog chrome allowance in rem so it scales with the root font Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: decode mermaid entity codes when sampling text for the font preload Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: match mermaid's decimal-only entity codes and decode the Inter sample too Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: drop emoji format characters from the font sample Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: state the emoji subset spread and modifier exclusions precisely Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: re-render once fonts settle instead of hand-picking emoji subsets Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor: give Modal a fill-height mode instead of measuring its chrome Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: guard the post-fonts re-render against a newer render The re-render after document.fonts.ready assigned svg without re-checking renderSeq after its own await. renderedCode is set before that await, so a stale re-render landing last leaves svg holding the previous diagram while renderedCode names the current source — showSvg stays true and paints the wrong diagram. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,11 @@
|
||||
style?: string
|
||||
cancelText?: string | undefined
|
||||
kind?: 'button' | 'X'
|
||||
/** Make the dialog fill the height it is anchored to and lay its body out as a flex
|
||||
* column, so content can size itself with `h-full` / `flex-1 min-h-0`. Off by default:
|
||||
* the dialog otherwise hugs its content, and percentage heights inside it do not
|
||||
* resolve (the centering wrapper is `min-h-full`, i.e. height:auto). */
|
||||
fillHeight?: boolean
|
||||
/** Force a minimum z-index base. Defaults to elevating above the AI chat
|
||||
* side panel when it is open. Pass an explicit value to stack above other
|
||||
* surfaces (e.g. a modal opened over the /sessions preview-pane editor). */
|
||||
@@ -36,6 +41,7 @@
|
||||
style = '',
|
||||
cancelText = undefined,
|
||||
kind = 'button',
|
||||
fillHeight = false,
|
||||
minZIndex: minZIndexProp = undefined,
|
||||
settings,
|
||||
children: children_render,
|
||||
@@ -119,12 +125,17 @@
|
||||
></div>
|
||||
|
||||
<div class="{posClass} inset-0 z-10 overflow-y-auto">
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<div
|
||||
class="flex {fillHeight ? 'h-full' : 'min-h-full'} items-center justify-center p-4"
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
onclick={stopPropagation(bubble('click'))}
|
||||
class={twMerge(
|
||||
'relative transform overflow-hidden rounded-md bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6',
|
||||
'relative transform overflow-hidden rounded-md bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:w-full sm:max-w-lg sm:p-6',
|
||||
// The margins are what keeps a content-sized dialog off the viewport edges; a
|
||||
// filling one takes its inset from the wrapper's padding instead.
|
||||
fillHeight ? 'h-full flex flex-col' : 'sm:my-8',
|
||||
c,
|
||||
open
|
||||
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
|
||||
@@ -137,16 +148,16 @@
|
||||
><CloseButton on:close={() => (open = false)} /></div
|
||||
>
|
||||
{/if}
|
||||
<div class="flex">
|
||||
<div class="flex {fillHeight ? 'flex-1 min-h-0' : ''}">
|
||||
<!-- min-w-0: without it this flex item takes its content's min-content width and
|
||||
stretches the modal past its max-width instead of letting content shrink. -->
|
||||
<div class="text-left flex-1 min-w-0">
|
||||
<div class="text-left flex-1 min-w-0 {fillHeight ? 'flex flex-col min-h-0' : ''}">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<h3 class="text-emphasis text-lg font-semibold">{title}</h3>
|
||||
{@render settings?.()}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-primary">
|
||||
<div class="mt-4 text-sm text-primary {fillHeight ? 'flex-1 min-h-0' : ''}">
|
||||
{@render children_render?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,13 @@
|
||||
startOnLoad: false,
|
||||
theme: dark ? 'dark' : 'default',
|
||||
securityLevel: 'strict',
|
||||
// Mermaid writes this stack into a <style> block inside the SVG, so the app's font
|
||||
// never reaches a diagram: the bundled vector emoji font has to be named here or
|
||||
// emoji fall back to the platform bitmap one, which ignores the zoom transform
|
||||
// (app.css). Inter sits last before it because the emoji font also covers digits,
|
||||
// # and *; on a box without the MS core fonts those would otherwise render as its
|
||||
// keycap glyphs. Keeping Inter behind them leaves diagram typography unchanged.
|
||||
fontFamily: '"trebuchet ms", verdana, arial, Inter, "Noto Color Emoji", sans-serif',
|
||||
// Throw on parse errors instead of injecting an orphan error diagram into the DOM.
|
||||
suppressErrorRendering: true
|
||||
})
|
||||
@@ -43,6 +50,17 @@
|
||||
if (seq !== renderSeq) return
|
||||
svg = result.svg
|
||||
renderedCode = source
|
||||
// Mermaid sizes each node by measuring its label, so a font still in flight yields
|
||||
// boxes cut to fallback metrics that the real glyphs then overflow. Drawing the
|
||||
// diagram is itself what asks for the fonts its glyphs need — including the emoji
|
||||
// subsets — so let them settle and lay it out again against the true metrics.
|
||||
if (document.fonts?.status === 'loading') {
|
||||
await document.fonts.ready
|
||||
if (seq !== renderSeq) return
|
||||
const settled = await mermaid.render(`mermaid-${randomUUID()}`, source)
|
||||
if (seq !== renderSeq) return
|
||||
svg = settled.svg
|
||||
}
|
||||
} catch {
|
||||
// Parse failure (often a partial block still streaming in): fall back to the
|
||||
// raw source. `showSvg` already hides any previous diagram since `renderedCode`
|
||||
@@ -145,7 +163,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal bind:open={expanded} title="Diagram" kind="X" class="sm:max-w-none w-[92vw]">
|
||||
<Modal bind:open={expanded} title="Diagram" kind="X" fillHeight class="sm:max-w-none w-[92vw]">
|
||||
{#snippet settings()}
|
||||
<div class="flex flex-row gap-1 mr-8">
|
||||
<Button
|
||||
@@ -183,7 +201,7 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
<div
|
||||
class="relative w-full h-[78vh] overflow-hidden rounded border cursor-grab bg-surface-secondary"
|
||||
class="relative w-full h-full overflow-hidden rounded border cursor-grab bg-surface-secondary"
|
||||
>
|
||||
{#if expanded}
|
||||
<div use:panzoomAction class="w-full h-full flex items-center justify-center">
|
||||
|
||||
@@ -127,14 +127,16 @@
|
||||
active ? 'z-10 opacity-100 pointer-events-auto' : 'z-0 opacity-0 pointer-events-none'
|
||||
)
|
||||
|
||||
// Overlays the editor opens (drawers, modals, popovers) anchor here rather than to the
|
||||
// Overlays a tab opens (drawers, modals, popovers) anchor here rather than to the
|
||||
// document, so they stay within this tab and hide with it when another tab takes over.
|
||||
// Every branch that renders content in-realm must bind this — an unbound host makes the
|
||||
// overlay fall back to viewport-`fixed`, spilling it across the whole app.
|
||||
// The stack is per-tab for the same reason: this host stays mounted while hidden, and a
|
||||
// shared stack would let its overlays arbitrate Escape for the tab the user is looking at.
|
||||
let editorEl: HTMLDivElement | undefined = $state()
|
||||
let overlayHostEl: HTMLDivElement | undefined = $state()
|
||||
let hostDrawers = $state({ val: [] as string[] })
|
||||
setOverlayHost({
|
||||
el: () => editorEl,
|
||||
el: () => overlayHostEl,
|
||||
drawers: hostDrawers,
|
||||
// The panel is resized rather than unmounted, so the active tab of a panel that
|
||||
// is off screen is as invisible as a background tab.
|
||||
@@ -180,7 +182,7 @@
|
||||
|
||||
{#if slot.kind === 'editor' && mounted && runtime}
|
||||
<div
|
||||
bind:this={editorEl}
|
||||
bind:this={overlayHostEl}
|
||||
class="absolute inset-0 flex flex-col min-h-0 bg-surface {visibility}"
|
||||
aria-hidden={!active}
|
||||
>
|
||||
@@ -238,7 +240,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else if slot.kind === 'artifact' && mounted}
|
||||
<div class="absolute inset-0 flex flex-col min-h-0 bg-surface {visibility}" aria-hidden={!active}>
|
||||
<div
|
||||
bind:this={overlayHostEl}
|
||||
class="absolute inset-0 flex flex-col min-h-0 bg-surface {visibility}"
|
||||
aria-hidden={!active}
|
||||
>
|
||||
{#if artifact}
|
||||
<ArtifactViewer {artifact} />
|
||||
{:else if !runtime?.manager.artifacts.loading}
|
||||
|
||||
Reference in New Issue
Block a user