Editor terminal resizing

This commit is contained in:
ernstmul
2025-05-15 15:38:51 +02:00
parent f0b09610ee
commit 27e80dfaa1
3 changed files with 229 additions and 149 deletions
+2 -2
View File
@@ -73,8 +73,8 @@
const resizer = throttle(() => {
if (!editorElement) return;
editor.layout({
height: editorElement.offsetHeight,
width: editorElement.offsetWidth
height: editorElement.parentElement.offsetHeight,
width: editorElement.parentElement.offsetWidth
});
}, 50);
const observer = new ResizeObserver(resizer);
@@ -40,5 +40,6 @@
display: flex;
flex-direction: column;
gap: 0;
overflow-y: scroll;
}
</style>
@@ -15,6 +15,7 @@
import {
disableBodySelect,
enabledBodySelect,
getChatWidthFromPrefs,
getTerminalHeightFromPrefs,
getTerminalOpenFromPrefs,
saveTerminalHeightToPrefs,
@@ -69,12 +70,27 @@
let terminalOpen = $state(getTerminalOpenFromPrefs());
let asideRef: HTMLElement;
let isResizing = false;
let terminalHeight = $state(getTerminalHeightFromPrefs());
const minHeight = 350;
let resizerTopPosition = $state(minHeight);
const terminalTabsHeight = 50;
let layoutElement = $state<HTMLDivElement | null>(null);
let editorHeight = $derived(resizerTopPosition + 1);
let terminalHeight = $derived(
layoutElement.offsetHeight - resizerTopPosition - terminalTabsHeight
);
$effect(() => {
if (terminalOpen !== undefined) {
saveTerminalOpenToPrefs(terminalOpen);
}
if (terminalOpen === false) {
editorHeight = layoutElement.offsetHeight - 46;
} else if (terminalOpen) {
editorHeight = resizerTopPosition + 1;
terminalHeight = layoutElement.offsetHeight - resizerTopPosition - terminalTabsHeight;
}
});
function startResize() {
@@ -91,17 +107,25 @@
function resize(event: MouseEvent | TouchEvent) {
if (!isResizing) return;
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
const relativeY = clientY - 50; // y distance from top of card
const maxHeight = window.innerHeight - 400;
if (relativeY < minHeight) {
resizerTopPosition = minHeight;
} else if (relativeY > maxHeight) {
resizerTopPosition = maxHeight;
} else {
resizerTopPosition = relativeY;
}
let height =
asideRef.getBoundingClientRect().y -
clientY +
asideRef.getBoundingClientRect().height -
32;
terminalHeight = height < maxHeight ? height : maxHeight;
// let height =
// asideRef.getBoundingClientRect().y -
// clientY +
// asideRef.getBoundingClientRect().height -
// 32;
//
// terminalHeight = height < maxHeight ? height : maxHeight;
}
function stopResize() {
@@ -151,6 +175,12 @@
});
return mappedArtifacts;
});
$effect(() => {
if (view === 'editor' && terminals.size === 0) {
createTerminal();
}
});
</script>
{#snippet artifactSelector()}
@@ -159,140 +189,159 @@
{/if}
{/snippet}
<Layout.Stack
direction="column"
height={$isSmallViewport ? 'calc(100vh - 119px)' : 'calc(100vh - 74px)'}
gap="none">
<Layout.Stack direction="column" gap="none">
{#if $isSmallViewport}
<Layout.Stack direction="row" gap="none" justifyContent="center">
<div>
{@render artifactSelector()}
<div bind:this={layoutElement} class="layout">
<Layout.Stack
direction="column"
height={$isSmallViewport ? 'calc(100vh - 119px)' : 'calc(100vh - 79px)'}
gap="none">
<Layout.Stack direction="column" gap="none">
{#if $isSmallViewport}
<Layout.Stack direction="row" gap="none" justifyContent="center">
<div>
{@render artifactSelector()}
</div>
</Layout.Stack>
<div class="divider-wrapper-artifacts">
<Divider />
</div>
</Layout.Stack>
<div class="divider-wrapper-artifacts">
<Divider />
</div>
{/if}
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack gap="xxs" direction="row" alignItems="center" inline>
{#if !$showChat}
<Button.Button
variant="compact"
style="--p-button-padding-block:0; margin-inline-start: -2px; margin-inline-end: 4px; color:var(--fgcolor-neutral-tertiary)"
on:click={() => {
showChat.set(true);
}}><Icon icon={IconChatLayout} size="l"></Icon></Button.Button>
{/if}
<Tabs>
<Tab
selected={view === 'preview'}
on:click={() => {
view = 'preview';
}}>Preview</Tab>
<Tab
selected={view === 'editor'}
on:click={() => {
view = 'editor';
}}>Code</Tab>
</Tabs>
</Layout.Stack>
{#if !$isSmallViewport}
{@render artifactSelector()}
{/if}
<Layout.Stack gap="s" direction="row" alignItems="center" inline>
<Button.Button size="s" variant="primary">Release</Button.Button>
</Layout.Stack>
</Layout.Stack>
<div class="divider-wrapper">
<Divider />
</div>
</Layout.Stack>
<div style:display={view === 'preview' ? 'contents' : 'none'}>
{@render children()}
</div>
<div style:display={view === 'editor' ? 'contents' : 'none'}>
<Code />
</div>
<aside bind:this={asideRef}>
<details bind:open={terminalOpen}>
<summary
onmousedown={startResize}
ontouchmove={startResize}
onclick={(event) => {
event.preventDefault();
}}
class:terminal-slider={terminalOpen}>
<div class="terminal-tabs">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center">
<Layout.Stack direction="row" gap="s">
<Tabs let:root>
<Tab
{root}
selected={currentTerminal === mainTerminalId}
on:click={() => {
currentTerminal = mainTerminalId;
terminalOpen = true;
}}>
<Icon icon={IconImagine} />
Imagine
</Tab>
{#each terminals as [symbol] (symbol)}
<Tab
{root}
on:click={() => (currentTerminal = symbol)}
selected={currentTerminal === symbol}>
<Icon icon={IconTerminal} />
Terminal
</Tab>
{/each}
</Tabs>
<Button.Button
variant="text"
size="s"
on:click={(event) => {
event.preventDefault();
createTerminal();
}}
icon>
<Icon
icon={IconPlusSm}
size="m"
color="--fgcolor-neutral-tertiary" />
</Button.Button>
</Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack gap="xxs" direction="row" alignItems="center" inline>
{#if !$showChat}
<Button.Button
variant="compact"
onclick={() => {
terminalOpen = !terminalOpen;
}}>
<Icon
icon={terminalOpen ? IconChevronDoubleDown : IconChevronDoubleUp}
color="--fgcolor-neutral-tertiary" />
</Button.Button>
</Layout.Stack>
</div>
</summary>
style="--p-button-padding-block:0; margin-inline-start: -2px; margin-inline-end: 4px; color:var(--fgcolor-neutral-tertiary)"
on:click={() => {
showChat.set(true);
}}><Icon icon={IconChatLayout} size="l"></Icon></Button.Button>
{/if}
<Tabs>
<Tab
selected={view === 'preview'}
on:click={() => {
view = 'preview';
}}>Preview</Tab>
<Tab
selected={view === 'editor'}
on:click={() => {
view = 'editor';
}}>Code</Tab>
</Tabs>
</Layout.Stack>
{#if !$isSmallViewport}
{@render artifactSelector()}
{/if}
{#key page.params.artifact}
<div style:display={currentTerminal === mainTerminalId ? 'contents' : 'none'}>
<Terminal height={terminalHeight} {synapse}></Terminal>
</div>
{/key}
{#each terminals as [symbol, synapse] (symbol)}
<div style:display={currentTerminal === symbol ? 'contents' : 'none'}>
<Terminal height={terminalHeight} {synapse}></Terminal>
</div>
{/each}
</details>
</aside>
</Layout.Stack>
<Layout.Stack gap="s" direction="row" alignItems="center" inline>
<Button.Button size="s" variant="primary">Release</Button.Button>
</Layout.Stack>
</Layout.Stack>
<div class="divider-wrapper">
<Divider />
</div>
</Layout.Stack>
<div style:display={view === 'preview' ? 'contents' : 'none'}>
{@render children()}
</div>
<div style:display={view === 'editor' ? 'contents' : 'none'}>
<div class="code-container" style:height={editorHeight - 50 + 'px'}>
<Code />
</div>
</div>
{#if view === 'editor'}
{#if terminalOpen}
<div
class="resizer"
role="presentation"
style:top={`${resizerTopPosition}px`}
onmousedown={startResize}
ontouchmove={startResize} />
{/if}
<aside bind:this={asideRef} style:top={`${editorHeight}px`}>
<details bind:open={terminalOpen}>
<summary
onclick={(event) => {
event.preventDefault();
}}>
<div class="terminal-tabs">
<Layout.Stack
direction="row"
justifyContent="space-between"
alignItems="center">
<Layout.Stack direction="row" gap="s">
<Tabs let:root>
<Tab
{root}
selected={currentTerminal === mainTerminalId}
on:click={() => {
currentTerminal = mainTerminalId;
terminalOpen = true;
}}>
<Icon icon={IconImagine} />
Imagine
</Tab>
{#each terminals as [symbol] (symbol)}
<Tab
{root}
on:click={() => (currentTerminal = symbol)}
selected={currentTerminal === symbol}>
<Icon icon={IconTerminal} />
Terminal
</Tab>
{/each}
</Tabs>
<Button.Button
variant="text"
size="s"
on:click={(event) => {
event.preventDefault();
createTerminal();
}}
icon>
<Icon
icon={IconPlusSm}
size="m"
color="--fgcolor-neutral-tertiary" />
</Button.Button>
</Layout.Stack>
<Button.Button
variant="compact"
onclick={() => {
terminalOpen = !terminalOpen;
}}>
<Icon
icon={terminalOpen
? IconChevronDoubleDown
: IconChevronDoubleUp}
color="--fgcolor-neutral-tertiary" />
</Button.Button>
</Layout.Stack>
</div>
</summary>
<style>
{#key page.params.artifact}
<div
style:display={currentTerminal === mainTerminalId
? 'contents'
: 'none'}>
<Terminal height={terminalHeight} {synapse}></Terminal>
</div>
{/key}
{#each terminals as [symbol, synapse] (symbol)}
<div style:display={currentTerminal === symbol ? 'contents' : 'none'}>
<Terminal height={terminalHeight} {synapse}></Terminal>
</div>
{/each}
</details>
</aside>
{/if}
</Layout.Stack>
</div>
<style lang="scss">
.layout {
position: relative;
}
aside {
background-color: var(--bgcolor-neutral-default);
@@ -301,28 +350,51 @@
width: 100%;
position: fixed;
position: absolute;
bottom: 67px;
@media (min-width: 768px) {
width: calc(100% + 2 * var(--space-7));
margin-inline-start: calc(-1 * var(--space-7));
margin-inline-start: -15px;
border-bottom-right-radius: var(--border-radius-m);
position: static;
}
}
.terminal-slider {
cursor: row-resize;
border-top: 1px solid var(--border-neutral);
transition: all 0.2s ease-in-out;
//.terminal-slider {
// cursor: row-resize;
// border-top: 1px solid var(--border-neutral);
// transition: all 0.2s ease-in-out;
//
// &:hover {
// border-top: 2px solid var(--border-neutral-strong);
// background-color: var(--border-neutral-strong);
// }
//}
&:hover {
border-top: 2px solid var(--border-neutral-strong);
.resizer {
position: absolute;
height: 16px;
cursor: row-resize;
margin-inline-start: calc(-1 * var(--space-7));
width: calc(100% + 2 * var(--space-7));
z-index: 1;
&::after {
content: '';
cursor: row-resize;
position: absolute;
height: 1px;
width: 100%;
background-color: var(--border-neutral);
opacity: 1;
transition: all 0.3s ease-in-out;
}
&:hover::after {
height: 2px;
background-color: var(--border-neutral-strong);
}
}
.terminal-tabs {
background-color: var(--bgcolor-neutral-primary);
padding: var(--space-2) var(--space-7);
@@ -348,4 +420,11 @@
width: calc(100% + var(--space-10));
}
}
.code-container {
@media (min-width: 768px) {
margin-inline-start: calc(-1 * var(--space-7));
width: calc(100% + 2 * var(--space-7));
}
}
</style>