mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Editor terminal resizing
This commit is contained in:
@@ -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>
|
||||
|
||||
+226
-147
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user