mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-19 16:03:29 +00:00
style(raw_apps): trim verbose comments
This commit is contained in:
@@ -15,12 +15,9 @@
|
||||
pinned?: 'left' | 'right'
|
||||
}
|
||||
|
||||
// Per-instance counter so every DraggableTabs gets its own dnd zone `type`.
|
||||
// svelte-dnd-action treats zones sharing a `type` as one connected pool:
|
||||
// two bars rendering the same tab ids (e.g. the mirrored single-view bars)
|
||||
// would let a drag in one bar mark the matching item as a hidden shadow
|
||||
// placeholder in the other and never clear it. Unique types keep them
|
||||
// independent. We never drag tabs between two bars, so this loses nothing.
|
||||
// Per-instance dnd zone `type` so sibling bars (mirrored single-view) don't
|
||||
// share svelte-dnd-action's item pool — otherwise a drag in one ghosts the
|
||||
// matching tab in the other.
|
||||
let dndZoneSeq = 0
|
||||
</script>
|
||||
|
||||
@@ -52,15 +49,9 @@
|
||||
// Unique dnd zone type for this instance (see note in the module block).
|
||||
const dndType = `draggable-tabs-${dndZoneSeq++}`
|
||||
|
||||
// Local copy of the draggable items that the dnd zone owns and renders.
|
||||
// We deliberately do NOT push `handleConsider` updates back to the parent:
|
||||
// mid-drag, svelte-dnd-action injects a hidden "shadow placeholder" item
|
||||
// into `e.detail.items`. If that reached the parent `tabs`, any sibling
|
||||
// DraggableTabs bound to the same `tabs` (e.g. the mirrored single-view
|
||||
// bars) would render the placeholder as a `visibility:hidden` ghost and
|
||||
// keep it after the drag — the disappearing-tab bug. So consider only
|
||||
// updates this local list (live feedback for the dragged bar); we commit
|
||||
// to the parent on finalize, when the placeholder is already gone.
|
||||
// Local list the dnd zone owns. `consider` updates only this (mid-drag it
|
||||
// holds svelte-dnd-action's shadow placeholder); we commit to the parent on
|
||||
// `finalize` so the placeholder never leaks into a sibling bar.
|
||||
let dndMiddle = $state<TabItem[]>(untrack(() => middle))
|
||||
let isDragging = false
|
||||
$effect(() => {
|
||||
@@ -69,22 +60,14 @@
|
||||
if (!isDragging) dndMiddle = next
|
||||
})
|
||||
|
||||
// Melt scroll-area: type='hover' reveals the custom 4px horizontal bar
|
||||
// only while the user hovers the tab strip (or scrolls it), then hides it
|
||||
// after `hideDelay`. melt flips the scrollbar's data-state, which our CSS
|
||||
// fades. The bar is absolutely positioned and its gutter is reserved by
|
||||
// the root's bottom padding, so showing/hiding it never shifts the layout.
|
||||
// `type: 'hover'` shows the custom bar only while hovering/scrolling the strip.
|
||||
const {
|
||||
elements: { root, viewport, content, scrollbarX, thumbX }
|
||||
} = createScrollArea({ type: 'hover', hideDelay: 600, dir: 'ltr' })
|
||||
|
||||
// melt's scroll-area only recomputes the thumb from a ResizeObserver on its
|
||||
// *content* element, never the viewport. So resizing the pane (viewport
|
||||
// shrinks/grows while the fixed-width tabs don't) leaves the thumb size and
|
||||
// overflow state stale — a known melt limitation. We detect viewport width
|
||||
// changes with `bind:clientWidth` below and nudge melt: perturb the content
|
||||
// box by 1px (then revert) so its observer fires and recomputes against the
|
||||
// new viewport. The sentinel is 0×0, so nothing is visible.
|
||||
// melt only re-measures the thumb when its *content* resizes, not the
|
||||
// viewport — so a pane resize leaves the thumb stale. Detect width changes
|
||||
// via `bind:clientWidth` and nudge melt by perturbing the 0×0 sentinel's box.
|
||||
let viewportWidth = $state(0)
|
||||
let resizeSentinel: HTMLSpanElement | undefined = $state(undefined)
|
||||
$effect(() => {
|
||||
@@ -210,8 +193,7 @@
|
||||
{@render tabButton(tab)}
|
||||
{/each}
|
||||
|
||||
<!-- 0×0 sentinel used to nudge melt's content ResizeObserver
|
||||
on pane resize (see the $effect above). -->
|
||||
<!-- resize nudge for melt (see the $effect above) -->
|
||||
<span
|
||||
bind:this={resizeSentinel}
|
||||
aria-hidden="true"
|
||||
@@ -234,17 +216,11 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* The melt viewport hides its own native scrollbar internally; we just
|
||||
need to give it a height so the content sits flush above the custom
|
||||
bar that lives at the bottom of the root. */
|
||||
.tabs-viewport {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 4px-tall custom horizontal scrollbar pinned to the bottom of the
|
||||
strip. It's absolutely positioned, so showing/hiding it never shifts
|
||||
the tab layout. Appearance is identical across OSes regardless of the
|
||||
native scrollbar setting. */
|
||||
/* Custom 4px bar, absolutely positioned so toggling it never shifts layout. */
|
||||
:global([data-melt-scroll-area-scrollbar].tabs-scrollbar) {
|
||||
height: 4px;
|
||||
background: transparent;
|
||||
@@ -252,20 +228,15 @@
|
||||
user-select: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
/* melt flips the scrollbar's data-state to "hidden" when it shouldn't be
|
||||
shown (with `type: 'hover'`, whenever the strip isn't hovered/scrolled);
|
||||
fade the whole bar (thumb included) out then. The thumb's own data-state
|
||||
is unrelated, so we key off the scrollbar here, not the thumb. */
|
||||
/* Hide the whole bar when melt marks it hidden (key off the scrollbar, not
|
||||
the thumb — the thumb's data-state doesn't track overflow). */
|
||||
:global([data-melt-scroll-area-scrollbar].tabs-scrollbar[data-state='hidden']) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
:global([data-melt-scroll-area-thumb].tabs-thumb) {
|
||||
/* melt sizes the thumb via inline `height: var(--melt-scroll-area-thumb-height)`,
|
||||
but only populates the WIDTH var for a horizontal scrollbar — the height
|
||||
var stays empty, so the inline height collapses to 0 and the thumb is
|
||||
invisible. Supply the cross-axis size here so the inline `var()` resolves
|
||||
to the full 4px track height. */
|
||||
/* melt leaves the thumb-height var empty for a horizontal bar, collapsing
|
||||
the inline height to 0 — supply it so the thumb fills the 4px track. */
|
||||
--melt-scroll-area-thumb-height: 100%;
|
||||
height: 100%;
|
||||
width: var(--melt-scroll-area-thumb-width);
|
||||
|
||||
@@ -226,43 +226,29 @@
|
||||
? 'file'
|
||||
: 'runnable'
|
||||
)
|
||||
// Per-pane tab lists.
|
||||
// - Single mode: BOTH panes mirror the FULL tab list — whichever pane is
|
||||
// visible (paneA when source is active, paneB when preview is active)
|
||||
// carries every tab so the user can always switch. This is the fix
|
||||
// for the bug where activating Preview hid every tab.
|
||||
// - Split mode: each pane shows its own subset (VS Code-style):
|
||||
// left = file/runnable tabs, right = just the Preview pseudo-header.
|
||||
// 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
|
||||
)
|
||||
const rightPaneTabs = $derived<TabItem[]>(
|
||||
splitWithPreview ? tabs.filter((t) => t.id === PREVIEW_TAB_ID) : tabs
|
||||
)
|
||||
// In split mode the Preview tab is permanently visible in the right pane;
|
||||
// the right bar always shows it as the active tab regardless of which
|
||||
// file/runnable tab is logically active in the left pane.
|
||||
// In split mode the right bar always highlights Preview, regardless of the
|
||||
// left pane's active file/runnable.
|
||||
const rightPaneActiveId = $derived(splitWithPreview ? PREVIEW_TAB_ID : activeTabId)
|
||||
const showSource = $derived(activeTabKind === 'file')
|
||||
const showRunnable = $derived(activeTabKind === 'runnable')
|
||||
// Lazy-mount the UI Builder iframe: when Preview is active and no file
|
||||
// has ever been opened, paneA is 0 wide, and mounting the iframe there
|
||||
// makes the VS Code workbench fail with "Unable to figure out browser
|
||||
// width and height". We mount it the first moment showSource becomes
|
||||
// true (paneA is at 100% then, so the workbench measures correctly)
|
||||
// and keep it mounted afterwards so tab switches don't reload.
|
||||
// Mount the UI Builder iframe the first time a file is shown (paneA has
|
||||
// width then; mounting it at 0-width breaks the VS Code workbench), and
|
||||
// keep it mounted so tab switches don't reload it.
|
||||
let iframeShouldMount = $state(false)
|
||||
$effect(() => {
|
||||
if (showSource) iframeShouldMount = true
|
||||
})
|
||||
|
||||
// Pane sizes for the inner content-area Splitpanes are a pure function of
|
||||
// the mode + active tab, so they're derived (no effects):
|
||||
// - Split mode + file/runnable active: both panes at the user's ratio
|
||||
// - Preview active: right pane full (preview full width)
|
||||
// - Single mode + file/runnable active: left pane full
|
||||
// `paneARatio` holds the user's last manual split drag (see the setter
|
||||
// in the left <Pane bind:size> below). Right size mirrors the left.
|
||||
// Inner pane sizes are a pure function of mode + active tab → derived.
|
||||
// `paneARatio` is the user's last manual split drag (set by rememberPaneDrag).
|
||||
let paneARatio = $state(50)
|
||||
const paneALeftSize = $derived(
|
||||
splitWithPreview && activeTabKind !== 'preview'
|
||||
@@ -272,9 +258,8 @@
|
||||
: 100
|
||||
)
|
||||
const paneBRightSize = $derived(100 - paneALeftSize)
|
||||
// Remember a manual splitter drag. Splitpanes writes the new left size
|
||||
// here; we keep it only when it's a genuine in-between split position
|
||||
// (the 0/100 collapsed/full states are programmatic, not user intent).
|
||||
// Keep a manual drag only when it's a genuine in-between split (0/100 are
|
||||
// the programmatic collapsed/full states, not user intent).
|
||||
function rememberPaneDrag(v: number) {
|
||||
if (splitWithPreview && activeTabKind !== 'preview' && v > 0 && v < 100) {
|
||||
paneARatio = v
|
||||
@@ -291,10 +276,7 @@
|
||||
return filePath.split('/').pop() || filePath
|
||||
}
|
||||
|
||||
// Pick the file to open by default on a fresh load so the split view
|
||||
// shows the editor + preview instead of a blank full-screen preview.
|
||||
// Prefer the main `App.*` component, then the `index.*` entry, then any
|
||||
// other source file, falling back to whatever exists.
|
||||
// Default file to open on load: prefer App.*, then index.*, then any source file.
|
||||
function pickDefaultFile(f: Record<string, string> | undefined): string | undefined {
|
||||
if (!f) return undefined
|
||||
const keys = Object.keys(f).filter((p) => !p.endsWith('/'))
|
||||
@@ -312,11 +294,8 @@
|
||||
function activateTab(id: string, opts?: { force?: boolean }) {
|
||||
const tab = tabs.find((t) => t.id === id)
|
||||
if (!tab) return
|
||||
// Clicking the Preview tab while in split mode is normally a visual
|
||||
// no-op (Preview is already permanently shown in the right pane;
|
||||
// promoting it would collapse the left pane). `force: true` is used
|
||||
// by closeTab's fallback when the only remaining tab IS Preview —
|
||||
// in that case we genuinely have nothing on the left to keep alive.
|
||||
// In split mode Preview is always shown on the right, so clicking it is a
|
||||
// no-op (would collapse the left pane). `force` lets closeTab fall back to it.
|
||||
if (!opts?.force && splitWithPreview && id === PREVIEW_TAB_ID) return
|
||||
activeTabId = id
|
||||
if (tab.id === PREVIEW_TAB_ID) {
|
||||
@@ -324,10 +303,8 @@
|
||||
} else if (tab.id.startsWith(FILE_PREFIX)) {
|
||||
const filePath = tab.id.slice(FILE_PREFIX.length)
|
||||
selectedRunnable = undefined
|
||||
// Always update `selectedDocument` — it drives sidebar highlight
|
||||
// and, crucially, is read by `populateFiles` once the iframe
|
||||
// finishes loading so a hydrated tab opens the right file even
|
||||
// when activateTab fires before the iframe's listener exists.
|
||||
// `populateFiles` reads this on iframe load, so set it even if the
|
||||
// iframe isn't ready yet (the postMessage below is then skipped).
|
||||
selectedDocument = filePath
|
||||
if (iframeLoaded) {
|
||||
iframe?.contentWindow?.postMessage({ type: 'selectFile', path: filePath }, '*')
|
||||
@@ -380,25 +357,20 @@
|
||||
const tab = tabs[idx]
|
||||
if (!tab || tab.closable === false) return
|
||||
const wasActive = activeTabId === id
|
||||
// Clear runnable selection BEFORE removing the tab so the $effect
|
||||
// doesn't immediately recreate it.
|
||||
// Clear selection before removal so the cleanup $effect doesn't recreate it.
|
||||
if (wasActive && tab.id.startsWith(RUNNABLE_PREFIX)) {
|
||||
selectedRunnable = undefined
|
||||
}
|
||||
tabs = tabs.filter((t) => t.id !== id)
|
||||
if (wasActive) {
|
||||
// Fall back to the previous tab, else the preview tab. Force
|
||||
// activation so the Preview-in-split-mode no-op guard doesn't
|
||||
// leave `activeTabId` pointing at the deleted tab.
|
||||
// Fall back to the previous tab (force, in case it's Preview in split).
|
||||
const fallback = tabs[Math.max(0, idx - 1)] ?? previewTab
|
||||
activateTab(fallback.id, { force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function reorderTabs(next: TabItem[]) {
|
||||
// `next` is `displayedTabs` after the user dropped a tab.
|
||||
// When split is on, the Preview tab isn't in `next`; re-append it
|
||||
// to preserve its slot in the underlying `tabs` array.
|
||||
// In split mode the right bar omits Preview from `next`; re-append it.
|
||||
if (splitWithPreview && !next.some((t) => t.id === PREVIEW_TAB_ID)) {
|
||||
tabs = [...next, previewTab]
|
||||
} else {
|
||||
@@ -411,10 +383,8 @@
|
||||
splitWithPreview = false
|
||||
return
|
||||
}
|
||||
// Going from single → split. The Preview tab is about to disappear
|
||||
// from the bar; if it's currently active, redirect to the last
|
||||
// file/runnable tab (or stay on preview if none exist — left pane
|
||||
// will be empty until the user opens something).
|
||||
// single → split: if Preview is active, move focus to the last
|
||||
// file/runnable tab (it's leaving the left bar).
|
||||
if (activeTabId === PREVIEW_TAB_ID) {
|
||||
const lastUserTab = tabs
|
||||
.slice()
|
||||
@@ -426,11 +396,8 @@
|
||||
}
|
||||
let yamlEditorDrawer: Drawer | undefined = $state(undefined)
|
||||
|
||||
// Sidebar uses a dynamic pixel-aware minimum: the user's `%` preference is
|
||||
// honored, but the pane can never shrink below SIDEBAR_PX_MIN — including
|
||||
// when the container itself shrinks. svelte-splitpanes' `minSize` only
|
||||
// blocks splitter drag, so we clamp `sidebarPanelSize` ourselves and use a
|
||||
// getter/setter bind so the user's preference is preserved.
|
||||
// Sidebar: honor the user's `%` but never shrink below SIDEBAR_PX_MIN.
|
||||
// (svelte-splitpanes' minSize only blocks drag, so we clamp ourselves.)
|
||||
let rawSidebarSize = $state(15)
|
||||
let splitContainerWidth = $state(0)
|
||||
const SIDEBAR_PX_MIN = 160
|
||||
@@ -1152,10 +1119,8 @@
|
||||
})
|
||||
})
|
||||
|
||||
// Open a sensible default file on mount so the split view shows the editor
|
||||
// + preview (and boots the UI Builder iframe) instead of a blank preview.
|
||||
// Tab/split layout is intentionally NOT persisted — each open starts fresh
|
||||
// (split mode, default file).
|
||||
// Open a default file on mount (boots the iframe; avoids a blank preview).
|
||||
// Layout isn't persisted — each open starts fresh in split mode.
|
||||
onMount(() => {
|
||||
if (tabs.length === 1) {
|
||||
const def = pickDefaultFile(files)
|
||||
@@ -1163,9 +1128,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
// Drop file tabs whose underlying file disappeared (deleted via setFiles
|
||||
// or workspace cleanup). Same for runnable tabs whose key no longer
|
||||
// exists in the runnables map.
|
||||
// Drop tabs whose file/runnable no longer exists.
|
||||
$effect(() => {
|
||||
void files
|
||||
void runnables
|
||||
|
||||
@@ -1,34 +1,9 @@
|
||||
/**
|
||||
* Helpers for using `svelte-splitpanes` with pixel-aware minimum widths.
|
||||
*
|
||||
* `svelte-splitpanes` only takes percentages for its `size`/`minSize`/`maxSize`
|
||||
* props, so when we need a pane to never shrink below a pixel threshold
|
||||
* (e.g. a sidebar that's unreadable under 160 px) we have to convert the
|
||||
* threshold into a percentage of the splitpane container's current width.
|
||||
*
|
||||
* Pattern at call sites:
|
||||
*
|
||||
* ```svelte
|
||||
* <div bind:clientWidth={containerWidth} class="h-full">
|
||||
* <Splitpanes>
|
||||
* <Pane minSize={paneMinPercent(containerWidth, 160)}>...</Pane>
|
||||
* </Splitpanes>
|
||||
* </div>
|
||||
* ```
|
||||
* `svelte-splitpanes` only takes percentages, so convert a pixel `minSize`
|
||||
* threshold into a % of the container's current width (pair with
|
||||
* `bind:clientWidth`). Capped at `cap`%; returns 0 until the width is known.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the percentage of `containerWidth` that corresponds to `minPx`,
|
||||
* capped at `cap` percent (default 80) so callers can't accidentally make
|
||||
* the pane fill the container in very narrow viewports.
|
||||
*
|
||||
* Returns 0 when the container width isn't yet known (initial render).
|
||||
*/
|
||||
export function paneMinPercent(
|
||||
containerWidth: number,
|
||||
minPx: number,
|
||||
cap: number = 80
|
||||
): number {
|
||||
export function paneMinPercent(containerWidth: number, minPx: number, cap: number = 80): number {
|
||||
if (containerWidth <= 0) return 0
|
||||
return Math.min(cap, (minPx / containerWidth) * 100)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user