style(raw_apps): trim verbose comments

This commit is contained in:
Guilhem Lemouel
2026-05-25 14:14:13 +02:00
parent 0587eae0ee
commit 1ed9a11f68
3 changed files with 46 additions and 137 deletions
@@ -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
+4 -29
View File
@@ -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)
}