From 42fe9601040dc20068d6d3cd84a908de769a88ce Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Thu, 21 May 2026 09:23:38 +0200 Subject: [PATCH] refactor(raw_apps): polish tab strip and sync editor font to text-xs --- .../common/tabs/DraggableTabs.svelte | 126 +++++++++++++----- .../components/raw_apps/RawAppEditor.svelte | 25 ++++ 2 files changed, 114 insertions(+), 37 deletions(-) diff --git a/frontend/src/lib/components/common/tabs/DraggableTabs.svelte b/frontend/src/lib/components/common/tabs/DraggableTabs.svelte index e68050bae8..3eef67998f 100644 --- a/frontend/src/lib/components/common/tabs/DraggableTabs.svelte +++ b/frontend/src/lib/components/common/tabs/DraggableTabs.svelte @@ -16,6 +16,7 @@ import { dndzone, type DndEvent } from '@windmill-labs/svelte-dnd-action' import { X } from 'lucide-svelte' import { twMerge } from 'tailwind-merge' + import { createScrollArea, melt } from '@melt-ui/svelte' interface Props { tabs: TabItem[] @@ -43,8 +44,15 @@ const middle = $derived(tabs.filter((t) => !t.pinned)) const pinnedRight = $derived(tabs.filter((t) => t.pinned === 'right')) + // Melt scroll-area: type='always' keeps the custom thumb's gutter present + // at all times, so the layout never shifts when content stops/starts + // overflowing. The native horizontal scrollbar is hidden by melt and our + // own 4px-tall track sits along the bottom of the strip. + const { + elements: { root, viewport, content, scrollbarX, thumbX } + } = createScrollArea({ type: 'always', dir: 'ltr' }) + function rebuild(middleNew: TabItem[]) { - // dnd-action only owns the middle (draggable) slice; reassemble. onReorder?.([...pinnedLeft, ...middleNew, ...pinnedRight]) } @@ -57,14 +65,10 @@ function tabClasses(isActive: boolean) { return twMerge( - 'group inline-flex items-center gap-1.5 px-3 h-8 text-xs select-none cursor-pointer whitespace-nowrap transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-border-selected focus-visible:ring-inset', - // Active tab shares its background with the content area below it - // — no border, the boundary "disappears" into the surface. - // Inactive tabs sit on the darker tab-strip bg and have a subtle - // right separator so they don't blur together. + 'group inline-flex items-center gap-1.5 px-2.5 h-7 text-xs rounded-md select-none cursor-pointer whitespace-nowrap transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-border-selected focus-visible:ring-inset', isActive - ? 'bg-surface text-primary' - : 'bg-surface-secondary text-secondary border-r border-border-light hover:bg-surface-hover hover:text-primary' + ? 'bg-surface-input text-primary' + : 'bg-transparent text-secondary hover:bg-surface-hover hover:text-primary' ) } @@ -88,7 +92,6 @@ } function handleAuxClick(e: MouseEvent, tab: TabItem) { - // Middle-click closes (browser convention). if (e.button === 1 && tab.closable !== false) { e.preventDefault() onClose?.(tab.id) @@ -130,41 +133,90 @@ {/snippet}
- {#each pinnedLeft as tab (tab.id)} - {@render tabButton(tab)} - {/each} +
+
+ +
+
+ {#each pinnedLeft as tab (tab.id)} + {@render tabButton(tab)} + {/each} -
- {#each middle as tab (tab.id)} -
- {@render tabButton(tab)} +
+ {#each middle as tab (tab.id)} +
+ {@render tabButton(tab)} +
+ {/each} +
+ + {#each pinnedRight as tab (tab.id)} + {@render tabButton(tab)} + {/each} +
- {/each} +
+ +
+
+
- {#each pinnedRight as tab (tab.id)} - {@render tabButton(tab)} - {/each} - {#if trailing} -
+
{@render trailing()}
{/if}
+ + diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index af18167f7e..90bb43e8c5 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -996,6 +996,22 @@ } let darkMode: boolean = $state(false) + // Host's computed `text-xs` size in px. Windmill bumps :root to 18px at + // ≥1760px viewports, so this re-evaluates on resize via the listener below. + let editorFontSize = $state(12) + function recomputeEditorFontSize() { + const rootPx = parseFloat( + getComputedStyle(document.documentElement).fontSize + ) + // text-xs is 0.75rem + editorFontSize = rootPx * 0.75 + } + $effect(() => { + recomputeEditorFontSize() + const onResize = () => recomputeEditorFontSize() + window.addEventListener('resize', onResize) + return () => window.removeEventListener('resize', onResize) + }) $effect(() => { iframe?.addEventListener('load', () => { iframeLoaded = true @@ -1030,6 +1046,15 @@ ) } }) + $effect(() => { + // Match VS Code's editor font size to Windmill's text-xs. + if (iframe && iframeLoaded) { + iframe.contentWindow?.postMessage( + { type: 'setFontSize', px: editorFontSize }, + '*' + ) + } + }) $effect(() => { iframe && iframeLoaded && files && populateFiles() })