Replace the Vue frontend with Svelte

This commit is contained in:
Keavon Chambers
2023-03-10 03:54:39 -08:00
parent e539e43483
commit 6e20ea538b
83 changed files with 10013 additions and 19687 deletions

View File

@@ -1,289 +1,307 @@
<script lang="ts">
import { defineComponent, nextTick, type PropType } from "vue";
<script lang="ts" context="module">
import Document from "@/components/panels/Document.svelte";
import IconButton from "@/components/widgets/buttons/IconButton.svelte";
import LayerTree from "@/components/panels/LayerTree.svelte";
import NodeGraph from "@/components/panels/NodeGraph.svelte";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.svelte";
import Properties from "@/components/panels/Properties.svelte";
import TextButton from "@/components/widgets/buttons/TextButton.svelte";
import { platformIsMac } from "@/utility-functions/platform";
import { type LayoutKeysGroup, type Key } from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import Document from "@/components/panels/Document.vue";
import LayerTree from "@/components/panels/LayerTree.vue";
import NodeGraph from "@/components/panels/NodeGraph.vue";
import Properties from "@/components/panels/Properties.vue";
import IconButton from "@/components/widgets/buttons/IconButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
const PANEL_COMPONENTS = {
Document,
IconButton,
LayerTree,
NodeGraph,
PopoverButton,
Properties,
TextButton,
};
type PanelTypes = keyof typeof PANEL_COMPONENTS;
export default defineComponent({
inject: ["editor"],
props: {
tabMinWidths: { type: Boolean as PropType<boolean>, default: false },
tabCloseButtons: { type: Boolean as PropType<boolean>, default: false },
tabLabels: { type: Array as PropType<{ name: string; tooltip?: string }[]>, required: true },
tabActiveIndex: { type: Number as PropType<number>, required: true },
panelType: { type: String as PropType<PanelTypes>, required: false },
clickAction: { type: Function as PropType<(index: number) => void>, required: false },
closeAction: { type: Function as PropType<(index: number) => void>, required: false },
},
methods: {
newDocument() {
this.editor.instance.newDocumentDialog();
},
openDocument() {
this.editor.instance.documentOpen();
},
platformModifiers(reservedKey: boolean): LayoutKeysGroup {
// TODO: Remove this by properly feeding these keys from a layout provided by the backend
const ALT: Key = { key: "Alt", label: "Alt" };
const COMMAND: Key = { key: "Command", label: "Command" };
const CONTROL: Key = { key: "Control", label: "Ctrl" };
if (platformIsMac()) return reservedKey ? [ALT, COMMAND] : [COMMAND];
return reservedKey ? [CONTROL, ALT] : [CONTROL];
},
async scrollTabIntoView(newIndex: number) {
await nextTick();
const panel: HTMLDivElement | undefined = this.$el;
if (!panel) return;
const newActiveTab = panel.querySelectorAll("[data-tab]")[newIndex] as HTMLDivElement | undefined;
newActiveTab?.scrollIntoView();
},
},
components: {
IconLabel,
LayoutCol,
LayoutRow,
TextLabel,
UserInputLabel,
...PANEL_COMPONENTS,
},
});
const PANEL_COMPONENTS = {
Document,
LayerTree,
NodeGraph,
Properties,
};
type PanelTypes = keyof typeof PANEL_COMPONENTS;
</script>
<template>
<LayoutCol class="panel">
<LayoutRow class="tab-bar" :class="{ 'min-widths': tabMinWidths }">
<LayoutRow class="tab-group" :scrollableX="true">
<script lang="ts">
import { getContext, tick } from "svelte";
import { platformIsMac, isEventSupported } from "@/utility-functions/platform";
import { type LayoutKeysGroup, type Key } from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.svelte";
import LayoutRow from "@/components/layout/LayoutRow.svelte";
import IconLabel from "@/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@/components/widgets/labels/TextLabel.svelte";
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.svelte";
import { type Editor } from "@/wasm-communication/editor";
const editor = getContext<Editor>("editor");
export let tabMinWidths = false;
export let tabCloseButtons = false;
export let tabLabels: { name: string; tooltip?: string }[];
export let tabActiveIndex: number;
export let panelType: PanelTypes | undefined = undefined;
export let clickAction: ((index: number) => void) | undefined = undefined;
export let closeAction: ((index: number) => void) | undefined = undefined;
let tabElements: LayoutRow[] = [];
function newDocument() {
editor.instance.newDocumentDialog();
}
function openDocument() {
editor.instance.documentOpen();
}
function platformModifiers(reservedKey: boolean): LayoutKeysGroup {
// TODO: Remove this by properly feeding these keys from a layout provided by the backend
const ALT: Key = { key: "Alt", label: "Alt" };
const COMMAND: Key = { key: "Command", label: "Command" };
const CONTROL: Key = { key: "Control", label: "Ctrl" };
if (platformIsMac()) return reservedKey ? [ALT, COMMAND] : [COMMAND];
return reservedKey ? [CONTROL, ALT] : [CONTROL];
}
export async function scrollTabIntoView(newIndex: number) {
await tick();
tabElements[newIndex].div().scrollIntoView();
}
</script>
<LayoutCol class="panel">
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }}>
<LayoutRow class="tab-group" scrollableX={true}>
{#each tabLabels as tabLabel, tabIndex (tabIndex)}
<LayoutRow
v-for="(tabLabel, tabIndex) in tabLabels"
:key="tabIndex"
class="tab"
:class="{ active: tabIndex === tabActiveIndex }"
:title="tabLabel.tooltip || null"
@click="(e: MouseEvent) => (e?.stopPropagation(), clickAction?.(tabIndex))"
@click.middle="(e: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))"
data-tab
classes={{ active: tabIndex === tabActiveIndex }}
tooltip={tabLabel.tooltip || undefined}
on:click={(e) => {
e.stopPropagation();
clickAction?.(tabIndex);
}}
on:auxclick={(e) => {
// Middle mouse button click
if (e.button === 1) {
e.stopPropagation();
closeAction?.(tabIndex);
}
}}
on:mouseup={(e) => {
// Fallback for Safari:
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#browser_compatibility
// The downside of using mouseup is that the mousedown didn't have to originate in the same element.
// A possible future improvement could save the target element during mousedown and check if it's the same here.
if (!isEventSupported("auxclick") && e.button === 1) {
e.stopPropagation();
closeAction?.(tabIndex);
}
}}
bind:this={tabElements[tabIndex]}
>
<TextLabel>{{ tabLabel.name }}</TextLabel>
<IconButton :action="(e?: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
<TextLabel>{tabLabel.name}</TextLabel>
{#if tabCloseButtons}
<IconButton
action={(e) => {
e?.stopPropagation();
closeAction?.(tabIndex);
}}
icon="CloseX"
size={16}
/>
{/if}
</LayoutRow>
</LayoutRow>
<PopoverButton :icon="'VerticalEllipsis'">
<TextLabel :bold="true">Panel Options</TextLabel>
<TextLabel :multiline="true">Coming soon</TextLabel>
</PopoverButton>
{/each}
</LayoutRow>
<LayoutCol class="panel-body">
<component :is="panelType" v-if="panelType" />
<LayoutCol class="empty-panel" v-else>
<PopoverButton icon="VerticalEllipsis">
<TextLabel bold={true}>Panel Options</TextLabel>
<TextLabel multiline={true}>Coming soon</TextLabel>
</PopoverButton>
</LayoutRow>
<LayoutCol class="panel-body">
{#if panelType}
<svelte:component this={PANEL_COMPONENTS[panelType]} />
{:else}
<LayoutCol class="empty-panel">
<LayoutCol class="content">
<LayoutRow class="logotype">
<IconLabel :icon="'GraphiteLogotypeSolid'" />
<IconLabel icon="GraphiteLogotypeSolid" />
</LayoutRow>
<LayoutRow class="actions">
<table>
<tr>
<td>
<TextButton :label="'New Document'" :icon="'File'" :action="() => newDocument()" />
<TextButton label="New Document" icon="File" action={() => newDocument()} />
</td>
<td>
<UserInputLabel :keysWithLabelsGroups="[[...platformModifiers(true), { key: 'KeyN', label: 'N' }]]" />
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
</td>
</tr>
<tr>
<td>
<TextButton :label="'Open Document'" :icon="'Folder'" :action="() => openDocument()" />
<TextButton label="Open Document" icon="Folder" action={() => openDocument()} />
</td>
<td>
<UserInputLabel :keysWithLabelsGroups="[[...platformModifiers(false), { key: 'KeyO', label: 'O' }]]" />
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
</td>
</tr>
</table>
</LayoutRow>
</LayoutCol>
</LayoutCol>
</LayoutCol>
{/if}
</LayoutCol>
</template>
</LayoutCol>
<style lang="scss">
.panel {
background: var(--color-1-nearblack);
border-radius: 6px;
overflow: hidden;
<style lang="scss" global>
.panel {
background: var(--color-1-nearblack);
border-radius: 6px;
overflow: hidden;
.tab-bar {
height: 28px;
min-height: auto;
.tab-bar {
height: 28px;
min-height: auto;
&.min-widths .tab-group .tab {
min-width: 120px;
max-width: 360px;
}
.tab-group {
flex: 1 1 100%;
position: relative;
// This always hangs out at the end of the last tab, providing 16px (15px plus the 1px reserved for the separator line) to the right of the tabs.
// When the last tab is selected, its bottom rounded fillet adds 16px to the width, which stretches the scrollbar width allocation in only that situation.
// This pseudo-element ensures we always reserve that space to prevent the scrollbar from jumping when the last tab is selected.
// There is unfortunately no apparent way to remove that 16px gap from the end of the scroll container, since negative margin does not reduce the scrollbar allocation.
&::after {
content: "";
width: 15px;
flex: 0 0 auto;
&.min-widths .tab-group .tab {
min-width: 120px;
max-width: 360px;
}
.tab {
flex: 0 1 auto;
height: 100%;
padding: 0 8px;
align-items: center;
.tab-group {
flex: 1 1 100%;
position: relative;
&.active {
background: var(--color-3-darkgray);
border-radius: 6px 6px 0 0;
// This always hangs out at the end of the last tab, providing 16px (15px plus the 1px reserved for the separator line) to the right of the tabs.
// When the last tab is selected, its bottom rounded fillet adds 16px to the width, which stretches the scrollbar width allocation in only that situation.
// This pseudo-element ensures we always reserve that space to prevent the scrollbar from jumping when the last tab is selected.
// There is unfortunately no apparent way to remove that 16px gap from the end of the scroll container, since negative margin does not reduce the scrollbar allocation.
&::after {
content: "";
width: 15px;
flex: 0 0 auto;
}
.tab {
flex: 0 1 auto;
height: 100%;
padding: 0 8px;
align-items: center;
position: relative;
&:not(:first-child)::before,
&::after {
content: "";
width: 16px;
height: 8px;
position: absolute;
bottom: 0;
&.active {
background: var(--color-3-darkgray);
border-radius: 6px 6px 0 0;
position: relative;
&:not(:first-child)::before,
&::after {
content: "";
width: 16px;
height: 8px;
position: absolute;
bottom: 0;
}
&:not(:first-child)::before {
left: -16px;
border-bottom-right-radius: 8px;
box-shadow: 8px 0 0 0 var(--color-3-darkgray);
}
&::after {
right: -16px;
border-bottom-left-radius: 8px;
box-shadow: -8px 0 0 0 var(--color-3-darkgray);
}
}
&:not(:first-child)::before {
left: -16px;
border-bottom-right-radius: 8px;
box-shadow: 8px 0 0 0 var(--color-3-darkgray);
span {
flex: 1 1 100%;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
// Height and line-height required because https://stackoverflow.com/a/21611191/775283
height: 100%;
line-height: 28px;
}
&::after {
right: -16px;
border-bottom-left-radius: 8px;
box-shadow: -8px 0 0 0 var(--color-3-darkgray);
.icon-button {
margin-left: 8px;
}
}
span {
flex: 1 1 100%;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
// Height and line-height required because https://stackoverflow.com/a/21611191/775283
height: 100%;
line-height: 28px;
}
& + .tab {
margin-left: 1px;
}
.icon-button {
margin-left: 8px;
}
& + .tab {
margin-left: 1px;
}
&:not(.active) + .tab:not(.active)::before {
content: "";
position: absolute;
left: -1px;
width: 1px;
height: 16px;
background: var(--color-4-dimgray);
}
&:last-of-type {
margin-right: 1px;
&:not(.active)::after {
&:not(.active) + .tab:not(.active)::before {
content: "";
position: absolute;
right: -1px;
left: -1px;
width: 1px;
height: 16px;
background: var(--color-4-dimgray);
}
&:last-of-type {
margin-right: 1px;
&:not(.active)::after {
content: "";
position: absolute;
right: -1px;
width: 1px;
height: 16px;
background: var(--color-4-dimgray);
}
}
}
}
.popover-button {
margin: 2px 4px;
}
}
.popover-button {
margin: 2px 4px;
}
}
.panel-body {
background: var(--color-3-darkgray);
flex: 1 1 100%;
flex-direction: column;
.panel-body {
background: var(--color-3-darkgray);
flex: 1 1 100%;
flex-direction: column;
.empty-panel {
background: var(--color-2-mildblack);
margin: 4px;
border-radius: 2px;
justify-content: center;
.empty-panel {
background: var(--color-2-mildblack);
margin: 4px;
border-radius: 2px;
justify-content: center;
.content {
flex: 0 0 auto;
align-items: center;
.content {
flex: 0 0 auto;
align-items: center;
.logotype {
margin-bottom: 40px;
.logotype {
margin-bottom: 40px;
svg {
width: auto;
height: 120px;
}
}
.actions {
table {
border-spacing: 8px;
margin: -8px;
td {
padding: 0;
svg {
width: auto;
height: 120px;
}
}
.text-button:not(:hover) {
background: none;
.actions {
table {
border-spacing: 8px;
margin: -8px;
td {
padding: 0;
}
.text-button:not(:hover) {
background: none;
}
}
}
}
}
}
}
}
</style>

View File

@@ -1,178 +1,170 @@
<script lang="ts">
import { defineComponent } from "vue";
import DialogModal from "@/components/floating-menus/DialogModal.svelte";
import LayoutCol from "@/components/layout/LayoutCol.svelte";
import LayoutRow from "@/components/layout/LayoutRow.svelte";
import Panel from "@/components/window/workspace/Panel.svelte";
import { getContext } from "svelte";
import { type Editor } from "@/wasm-communication/editor";
import { type WorkspaceState } from "@/state-providers/workspace";
import { type PortfolioState } from "@/state-providers/portfolio";
import { type DialogState } from "@/state-providers/dialog";
import { FrontendDocumentDetails } from "@/wasm-communication/messages";
import DialogModal from "@/components/floating-menus/DialogModal.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import Panel from "@/components/window/workspace/Panel.vue";
const MIN_PANEL_SIZE = 100;
const PANEL_SIZES = {
/**/ root: 100,
/* ├── */ content: 80,
/* │ ├── */ document: 60,
/* │ └── */ graph: 40,
/* └── */ details: 20,
/* ├── */ properties: 45,
/* └── */ layers: 55,
};
const MIN_PANEL_SIZE = 100;
const PANEL_SIZES = {
/**/ root: 100,
/* ├── */ content: 80,
/* │ ├── */ document: 60,
/* │ └── */ graph: 40,
/* └── */ details: 20,
/* ├── */ properties: 45,
/* └── */ layers: 55,
};
let panelSizes = PANEL_SIZES;
let documentPanel: Panel;
export default defineComponent({
inject: ["workspace", "portfolio", "dialog", "editor"],
data() {
return {
panelSizes: PANEL_SIZES,
$: activeDocumentIndex = $portfolio.activeDocumentIndex;
$: nodeGraphVisible = $workspace.nodeGraphVisible;
$: documentTabLabels = $portfolio.documents.map((doc: FrontendDocumentDetails) => {
const name = doc.displayName;
if (!editor.instance.inDevelopmentMode()) return { name };
const tooltip = `Document ID ${doc.id}`;
return { name, tooltip };
});
$: {
scrollIntoView(activeDocumentIndex);
}
function scrollIntoView(activeDocumentIndex: number) {
documentPanel?.scrollTabIntoView(activeDocumentIndex);
}
const editor = getContext<Editor>("editor");
const workspace = getContext<WorkspaceState>("workspace");
const portfolio = getContext<PortfolioState>("portfolio");
const dialog = getContext<DialogState>("dialog");
function resizePanel(e: PointerEvent) {
const gutter = (e.target || undefined) as HTMLDivElement | undefined;
const nextSibling = (gutter?.nextElementSibling || undefined) as HTMLDivElement | undefined;
const prevSibling = (gutter?.previousElementSibling || undefined) as HTMLDivElement | undefined;
const parentElement = (gutter?.parentElement || undefined) as HTMLDivElement | undefined;
const nextSiblingName = (nextSibling?.getAttribute("data-subdivision-name") || undefined) as keyof typeof PANEL_SIZES;
const prevSiblingName = (prevSibling?.getAttribute("data-subdivision-name") || undefined) as keyof typeof PANEL_SIZES;
if (!gutter || !nextSibling || !prevSibling || !parentElement || !nextSiblingName || !prevSiblingName) return;
// Are we resizing horizontally?
const isHorizontal = gutter.getAttribute("data-gutter-horizontal") !== null;
// Get the current size in px of the panels being resized and the gutter
const gutterSize = isHorizontal ? gutter.getBoundingClientRect().width : gutter.getBoundingClientRect().height;
const nextSiblingSize = isHorizontal ? nextSibling.getBoundingClientRect().width : nextSibling.getBoundingClientRect().height;
const prevSiblingSize = isHorizontal ? prevSibling.getBoundingClientRect().width : prevSibling.getBoundingClientRect().height;
const parentElementSize = isHorizontal ? parentElement.getBoundingClientRect().width : parentElement.getBoundingClientRect().height;
// Measure the resizing panels as a percentage of all sibling panels
const totalResizingSpaceOccupied = gutterSize + nextSiblingSize + prevSiblingSize;
const proportionBeingResized = totalResizingSpaceOccupied / parentElementSize;
// Prevent cursor flicker as mouse temporarily leaves the gutter
gutter.setPointerCapture(e.pointerId);
const mouseStart = isHorizontal ? e.clientX : e.clientY;
const updatePosition = (e: PointerEvent): void => {
const mouseCurrent = isHorizontal ? e.clientX : e.clientY;
let mouseDelta = mouseStart - mouseCurrent;
mouseDelta = Math.max(nextSiblingSize + mouseDelta, MIN_PANEL_SIZE) - nextSiblingSize;
mouseDelta = prevSiblingSize - Math.max(prevSiblingSize - mouseDelta, MIN_PANEL_SIZE);
panelSizes[nextSiblingName] = ((nextSiblingSize + mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
panelSizes[prevSiblingName] = ((prevSiblingSize - mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
window.dispatchEvent(new CustomEvent("resize"));
};
},
computed: {
activeDocumentIndex() {
return this.portfolio.state.activeDocumentIndex;
},
nodeGraphVisible() {
return this.workspace.state.nodeGraphVisible;
},
documentTabLabels() {
return this.portfolio.state.documents.map((doc) => {
const name = doc.displayName;
if (!this.editor.instance.inDevelopmentMode()) return { name };
const cleanup = (e: PointerEvent): void => {
gutter.releasePointerCapture(e.pointerId);
const tooltip = `Document ID ${doc.id}`;
return { name, tooltip };
});
},
},
methods: {
resizePanel(event: PointerEvent) {
const gutter = (event.target || undefined) as HTMLDivElement | undefined;
const nextSibling = (gutter?.nextElementSibling || undefined) as HTMLDivElement | undefined;
const prevSibling = (gutter?.previousElementSibling || undefined) as HTMLDivElement | undefined;
const parentElement = (gutter?.parentElement || undefined) as HTMLDivElement | undefined;
document.removeEventListener("pointermove", updatePosition);
document.removeEventListener("pointerleave", cleanup);
document.removeEventListener("pointerup", cleanup);
};
const nextSiblingName = (nextSibling?.getAttribute("data-subdivision-name") || undefined) as keyof typeof PANEL_SIZES;
const prevSiblingName = (prevSibling?.getAttribute("data-subdivision-name") || undefined) as keyof typeof PANEL_SIZES;
if (!gutter || !nextSibling || !prevSibling || !parentElement || !nextSiblingName || !prevSiblingName) return;
// Are we resizing horizontally?
const isHorizontal = gutter.getAttribute("data-gutter-horizontal") !== null;
// Get the current size in px of the panels being resized and the gutter
const gutterSize = isHorizontal ? gutter.getBoundingClientRect().width : gutter.getBoundingClientRect().height;
const nextSiblingSize = isHorizontal ? nextSibling.getBoundingClientRect().width : nextSibling.getBoundingClientRect().height;
const prevSiblingSize = isHorizontal ? prevSibling.getBoundingClientRect().width : prevSibling.getBoundingClientRect().height;
const parentElementSize = isHorizontal ? parentElement.getBoundingClientRect().width : parentElement.getBoundingClientRect().height;
// Measure the resizing panels as a percentage of all sibling panels
const totalResizingSpaceOccupied = gutterSize + nextSiblingSize + prevSiblingSize;
const proportionBeingResized = totalResizingSpaceOccupied / parentElementSize;
// Prevent cursor flicker as mouse temporarily leaves the gutter
gutter.setPointerCapture(event.pointerId);
const mouseStart = isHorizontal ? event.clientX : event.clientY;
const updatePosition = (event: PointerEvent): void => {
const mouseCurrent = isHorizontal ? event.clientX : event.clientY;
let mouseDelta = mouseStart - mouseCurrent;
mouseDelta = Math.max(nextSiblingSize + mouseDelta, MIN_PANEL_SIZE) - nextSiblingSize;
mouseDelta = prevSiblingSize - Math.max(prevSiblingSize - mouseDelta, MIN_PANEL_SIZE);
this.panelSizes[nextSiblingName] = ((nextSiblingSize + mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
this.panelSizes[prevSiblingName] = ((prevSiblingSize - mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
window.dispatchEvent(new CustomEvent("resize"));
};
const cleanup = (event: PointerEvent): void => {
gutter.releasePointerCapture(event.pointerId);
document.removeEventListener("pointermove", updatePosition);
document.removeEventListener("pointerleave", cleanup);
document.removeEventListener("pointerup", cleanup);
};
document.addEventListener("pointermove", updatePosition);
document.addEventListener("pointerleave", cleanup);
document.addEventListener("pointerup", cleanup);
},
},
watch: {
async activeDocumentIndex(newIndex: number) {
(this.$refs.documentPanel as typeof Panel | undefined)?.scrollTabIntoView(newIndex);
},
},
components: {
DialogModal,
LayoutCol,
LayoutRow,
Panel,
},
});
document.addEventListener("pointermove", updatePosition);
document.addEventListener("pointerleave", cleanup);
document.addEventListener("pointerup", cleanup);
}
</script>
<template>
<LayoutRow class="workspace" data-workspace>
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['root'] }" data-subdivision-name="root">
<LayoutCol class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['content'] }" data-subdivision-name="content">
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['document'] }" data-subdivision-name="document">
<Panel
:panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined"
:tabCloseButtons="true"
:tabMinWidths="true"
:tabLabels="documentTabLabels"
:clickAction="(tabIndex: number) => editor.instance.selectDocument(portfolio.state.documents[tabIndex].id)"
:closeAction="(tabIndex: number) => editor.instance.closeDocumentWithConfirmation(portfolio.state.documents[tabIndex].id)"
:tabActiveIndex="portfolio.state.activeDocumentIndex"
ref="documentPanel"
/>
<LayoutRow class="workspace" data-workspace>
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["root"] }} data-subdivision-name="root">
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["content"] }} data-subdivision-name="content">
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["document"] }} data-subdivision-name="document">
<Panel
panelType={$portfolio.documents.length > 0 ? "Document" : undefined}
tabCloseButtons={true}
tabMinWidths={true}
tabLabels={documentTabLabels}
clickAction={(tabIndex) => editor.instance.selectDocument($portfolio.documents[tabIndex].id)}
closeAction={(tabIndex) => editor.instance.closeDocumentWithConfirmation($portfolio.documents[tabIndex].id)}
tabActiveIndex={$portfolio.activeDocumentIndex}
bind:this={documentPanel}
/>
</LayoutRow>
{#if nodeGraphVisible}
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={resizePanel} />
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["graph"] }} data-subdivision-name="graph">
<Panel panelType="NodeGraph" tabLabels={[{ name: "Node Graph" }]} tabActiveIndex={0} />
</LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
<LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible" :style="{ 'flex-grow': panelSizes['graph'] }" data-subdivision-name="graph">
<Panel :panelType="'NodeGraph'" :tabLabels="[{ name: 'Node Graph' }]" :tabActiveIndex="0" />
</LayoutRow>
</LayoutCol>
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutCol>
<LayoutCol class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['details'] }" data-subdivision-name="details">
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['properties'] }" data-subdivision-name="properties">
<Panel :panelType="'Properties'" :tabLabels="[{ name: 'Properties' }]" :tabActiveIndex="0" />
</LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow>
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['layers'] }" data-subdivision-name="layers">
<Panel :panelType="'LayerTree'" :tabLabels="[{ name: 'Layer Tree' }]" :tabActiveIndex="0" />
</LayoutRow>
</LayoutCol>
</LayoutRow>
<DialogModal v-if="dialog.state.visible" />
{/if}
</LayoutCol>
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} />
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["properties"] }} data-subdivision-name="properties">
<Panel panelType="Properties" tabLabels={[{ name: "Properties" }]} tabActiveIndex={0} />
</LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["layers"] }} data-subdivision-name="layers">
<Panel panelType="LayerTree" tabLabels={[{ name: "Layer Tree" }]} tabActiveIndex={0} />
</LayoutRow>
</LayoutCol>
</LayoutRow>
</template>
{#if $dialog.visible}
<DialogModal />
{/if}
</LayoutRow>
<style lang="scss">
.workspace {
position: relative;
flex: 1 1 100%;
<style lang="scss" global>
.workspace {
position: relative;
flex: 1 1 100%;
.workspace-grid-subdivision {
min-height: 28px;
flex: 1 1 0;
.workspace-grid-subdivision {
min-height: 28px;
flex: 1 1 0;
&.folded {
flex-grow: 0;
height: 0;
&.folded {
flex-grow: 0;
height: 0;
}
}
.workspace-grid-resize-gutter {
flex: 0 0 4px;
&.layout-row {
cursor: ns-resize;
}
&.layout-col {
cursor: ew-resize;
}
}
}
.workspace-grid-resize-gutter {
flex: 0 0 4px;
&.layout-row {
cursor: ns-resize;
}
&.layout-col {
cursor: ew-resize;
}
}
}
</style>