mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
706 lines
22 KiB
Svelte
706 lines
22 KiB
Svelte
<script lang="ts">
|
|
import { getContext, onDestroy, tick } from "svelte";
|
|
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
|
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
|
import Data from "/src/components/panels/Data.svelte";
|
|
import Document from "/src/components/panels/Document.svelte";
|
|
import Layers from "/src/components/panels/Layers.svelte";
|
|
import Properties from "/src/components/panels/Properties.svelte";
|
|
import Welcome from "/src/components/panels/Welcome.svelte";
|
|
import IconButton from "/src/components/widgets/buttons/IconButton.svelte";
|
|
import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
|
|
import { panelDrag, startCrossPanelDrag, endCrossPanelDrag, updateCrossPanelHover, updateDockingHover } from "/src/stores/panel-drag";
|
|
import type { DockingEdge } from "/src/stores/panel-drag";
|
|
import type { DockingSplitDirection, EditorWrapper, PanelType } from "/wrapper/pkg/graphite_wasm_wrapper";
|
|
|
|
const PANEL_COMPONENTS = {
|
|
Welcome,
|
|
Document,
|
|
Layers,
|
|
Properties,
|
|
Data,
|
|
};
|
|
const BUTTON_LEFT = 0;
|
|
const BUTTON_MIDDLE = 1;
|
|
const BUTTON_RIGHT = 2;
|
|
const DRAG_ACTIVATION_DISTANCE = 5;
|
|
|
|
const editor = getContext<EditorWrapper>("editor");
|
|
|
|
export let tabMinWidths = false;
|
|
export let tabCloseButtons = false;
|
|
export let tabLabels: { name: string; unsaved?: boolean; tooltipLabel?: string; tooltipDescription?: string; tooltipShortcut?: string }[];
|
|
export let tabActiveIndex: number;
|
|
export let panelTypes: PanelType[];
|
|
export let panelId: string;
|
|
export let clickAction: ((index: number) => void) | undefined = undefined;
|
|
export let closeAction: ((index: number) => void) | undefined = undefined;
|
|
export let reorderAction: ((oldIndex: number, newIndex: number) => void) | undefined = undefined;
|
|
export let renameAction: ((index: number, newName: string) => void) | undefined = undefined;
|
|
export let emptySpaceAction: (() => void) | undefined = undefined;
|
|
export let crossPanelDropAction: ((sourcePanelId: string, targetPanelId: string, insertIndex: number) => void) | undefined = undefined;
|
|
export let groupDropAction: ((sourcePanelId: string, targetPanelId: string, insertIndex: number) => void) | undefined = undefined;
|
|
export let splitDropAction: ((targetPanelId: string, direction: DockingSplitDirection, tabs: PanelType[], activeTabIndex: number) => void) | undefined = undefined;
|
|
|
|
let className = "";
|
|
export { className as class };
|
|
export let classes: Record<string, boolean> = {};
|
|
let styleName = "";
|
|
export { styleName as style };
|
|
export let styles: Record<string, string | number | undefined> = {};
|
|
|
|
let tabElements: (LayoutRow | undefined)[] = [];
|
|
|
|
// Document tab rename state
|
|
let editingNameTabIndex: number | undefined = undefined;
|
|
let editingNameText = "";
|
|
let editingNameInputElement: HTMLInputElement | undefined = undefined;
|
|
|
|
async function setEditingTabName(tabIndex: number, currentName: string) {
|
|
editingNameText = currentName;
|
|
editingNameTabIndex = tabIndex;
|
|
|
|
await tick();
|
|
|
|
editingNameInputElement?.focus();
|
|
editingNameInputElement?.select();
|
|
}
|
|
|
|
function commitEditingTabName(event: Event) {
|
|
if (editingNameTabIndex === undefined || !(event.target instanceof HTMLInputElement)) return;
|
|
|
|
renameAction?.(editingNameTabIndex, event.target.value);
|
|
editingNameTabIndex = undefined;
|
|
}
|
|
|
|
// Tab drag-and-drop state
|
|
let dragStartState: { tabIndex: number; pointerX: number; pointerY: number; isGroupDrag: boolean } | undefined = undefined;
|
|
let dragging = false;
|
|
let insertionIndex: number | undefined = undefined;
|
|
let insertionMarkerLeft: number | undefined = undefined;
|
|
let lastPointerX = 0;
|
|
let tabGroupElement: LayoutRow | undefined = undefined;
|
|
|
|
onDestroy(() => {
|
|
endDrag();
|
|
});
|
|
|
|
function onEmptySpaceAction(e: MouseEvent) {
|
|
if (e.target !== e.currentTarget) return;
|
|
if (e.button === BUTTON_MIDDLE || (e.button === BUTTON_LEFT && e.detail === 2)) emptySpaceAction?.();
|
|
}
|
|
|
|
function tabBarPointerDown(e: PointerEvent) {
|
|
// Only start a group drag from the tab bar background (not from a tab or button)
|
|
if (e.button !== BUTTON_LEFT) return;
|
|
if (e.target !== e.currentTarget) return;
|
|
if (!crossPanelDropAction && !splitDropAction) return;
|
|
|
|
dragStartState = { tabIndex: tabActiveIndex, pointerX: e.clientX, pointerY: e.clientY, isGroupDrag: true };
|
|
dragging = false;
|
|
insertionIndex = undefined;
|
|
insertionMarkerLeft = undefined;
|
|
|
|
addDragListeners();
|
|
}
|
|
|
|
export async function scrollTabIntoView(newIndex: number) {
|
|
await tick();
|
|
tabElements[newIndex]?.div?.()?.scrollIntoView();
|
|
}
|
|
|
|
// Tab drag-and-drop handlers
|
|
|
|
function tabPointerDown(e: PointerEvent, tabIndex: number) {
|
|
if (e.button !== BUTTON_LEFT) return;
|
|
if (e.target instanceof Element && e.target.closest("[data-close-button]")) return;
|
|
|
|
// Activate the tab upon pointer down
|
|
clickAction?.(tabIndex);
|
|
|
|
// Allow within-panel reorder if there are multiple tabs, or cross-panel drag if this panel supports docking
|
|
const canReorder = reorderAction && tabLabels.length > 1;
|
|
const canCrossPanelDrag = crossPanelDropAction !== undefined;
|
|
if (!canReorder && !canCrossPanelDrag) return;
|
|
|
|
dragStartState = { tabIndex, pointerX: e.clientX, pointerY: e.clientY, isGroupDrag: false };
|
|
dragging = false;
|
|
insertionIndex = undefined;
|
|
insertionMarkerLeft = undefined;
|
|
|
|
addDragListeners();
|
|
}
|
|
|
|
function dragPointerMove(e: PointerEvent) {
|
|
if (!dragStartState) return;
|
|
|
|
// Activate drag after moving beyond threshold
|
|
if (!dragging) {
|
|
const deltaX = Math.abs(e.clientX - dragStartState.pointerX);
|
|
const deltaY = Math.abs(e.clientY - dragStartState.pointerY);
|
|
if (deltaX < DRAG_ACTIVATION_DISTANCE && deltaY < DRAG_ACTIVATION_DISTANCE) return;
|
|
|
|
dragging = true;
|
|
|
|
// Group drags enter cross-panel state for edge docking even without crossPanelDropAction
|
|
if (dragStartState.isGroupDrag && (crossPanelDropAction || splitDropAction)) {
|
|
startCrossPanelDrag(panelId, [...panelTypes], tabActiveIndex, true);
|
|
} else if (!dragStartState.isGroupDrag && crossPanelDropAction) {
|
|
const draggedTab = panelTypes[dragStartState.tabIndex];
|
|
startCrossPanelDrag(panelId, [draggedTab], dragStartState.tabIndex, false);
|
|
}
|
|
}
|
|
|
|
lastPointerX = e.clientX;
|
|
|
|
// Exit early in here after we show the insertion marker, if we're within our own tab bar
|
|
if (pointerIsInsideTabBar(e)) {
|
|
calculateInsertionIndex(lastPointerX);
|
|
updateCrossPanelHover(undefined, undefined, undefined);
|
|
return;
|
|
}
|
|
|
|
// Clear local insertion marker since we're outside our own tab bar
|
|
insertionIndex = undefined;
|
|
insertionMarkerLeft = undefined;
|
|
|
|
// Skip cross-panel hover detection for sources that can't dock anywhere
|
|
if (!crossPanelDropAction && !splitDropAction) return;
|
|
|
|
if (crossPanelDropAction) {
|
|
const tabBarTarget = Array.from(document.querySelectorAll("[data-panel-tab-bar]")).find((element) => {
|
|
const targetPanelId = element.getAttribute("data-panel-tab-bar");
|
|
if (!targetPanelId || targetPanelId === panelId) return false;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
return e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
|
|
});
|
|
|
|
const tabBarTargetId = tabBarTarget?.getAttribute("data-panel-tab-bar");
|
|
if (tabBarTarget instanceof HTMLDivElement && tabBarTargetId) {
|
|
calculateForeignInsertionIndex(e.clientX, tabBarTargetId, tabBarTarget);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check for edge-zone split docking
|
|
const panelBody = Array.from(document.querySelectorAll("[data-panel-body]")).find((element) => {
|
|
const rect = element.getBoundingClientRect();
|
|
return e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
|
|
});
|
|
|
|
const bodyPanelId = panelBody && panelBody.getAttribute("data-panel-body");
|
|
if (bodyPanelId) {
|
|
const rect = panelBody.getBoundingClientRect();
|
|
let edge: DockingEdge | undefined = detectDockingEdge(e.clientX, e.clientY, rect);
|
|
|
|
// Center drops between different panels require both to be cross-panel-dockable (self-drops are always allowed as a no-op)
|
|
if (edge === "Center" && bodyPanelId !== panelId) {
|
|
const targetIsDockable = panelBody.hasAttribute("data-panel-dockable");
|
|
const sourceIsDockable = crossPanelDropAction !== undefined;
|
|
if (!sourceIsDockable || !targetIsDockable) edge = undefined;
|
|
}
|
|
|
|
if (edge) {
|
|
updateDockingHover(bodyPanelId, edge);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Not hovering any drop target
|
|
updateCrossPanelHover(undefined, undefined, undefined);
|
|
updateDockingHover(undefined, undefined);
|
|
}
|
|
|
|
function dragPointerUp() {
|
|
if (dragging && dragStartState) {
|
|
const crossPanelState = $panelDrag;
|
|
|
|
// Center drop: append tabs to the target panel group
|
|
if (crossPanelState.active && crossPanelState.hoverDockingPanelId && crossPanelState.hoverDockingEdge === "Center") {
|
|
const dropAction = crossPanelState.draggingGroup ? groupDropAction : crossPanelDropAction;
|
|
dropAction?.(panelId, crossPanelState.hoverDockingPanelId, Number.MAX_SAFE_INTEGER);
|
|
}
|
|
// Edge docking drop: create a new split adjacent to the target panel
|
|
else if (crossPanelState.active && crossPanelState.hoverDockingPanelId && crossPanelState.hoverDockingEdge && crossPanelState.hoverDockingEdge !== "Center") {
|
|
splitDropAction?.(
|
|
crossPanelState.hoverDockingPanelId,
|
|
crossPanelState.hoverDockingEdge,
|
|
crossPanelState.draggedTabs,
|
|
crossPanelState.draggingGroup ? crossPanelState.sourceTabIndex : 0,
|
|
);
|
|
}
|
|
// Cross-panel tab bar drop: insert as a tab in the target panel group
|
|
else if (
|
|
crossPanelDropAction &&
|
|
crossPanelState.active &&
|
|
crossPanelState.hoverTargetPanelId &&
|
|
crossPanelState.hoverTargetPanelId !== panelId &&
|
|
crossPanelState.hoverInsertionIndex !== undefined
|
|
) {
|
|
const dropAction = crossPanelState.draggingGroup ? groupDropAction : crossPanelDropAction;
|
|
dropAction?.(panelId, crossPanelState.hoverTargetPanelId, crossPanelState.hoverInsertionIndex);
|
|
}
|
|
// Within-panel reorder
|
|
else if (insertionIndex !== undefined) {
|
|
const oldIndex = dragStartState.tabIndex;
|
|
|
|
// Adjust for the fact that removing the dragged tab shifts indices
|
|
let newIndex = insertionIndex;
|
|
if (newIndex > oldIndex) newIndex -= 1;
|
|
|
|
if (oldIndex !== newIndex) {
|
|
reorderAction?.(oldIndex, newIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
endDrag();
|
|
}
|
|
|
|
function dragAbort(e: MouseEvent | KeyboardEvent) {
|
|
if (e instanceof MouseEvent && e.button === BUTTON_RIGHT) endDrag();
|
|
if (e instanceof KeyboardEvent && e.key === "Escape") endDrag();
|
|
}
|
|
|
|
function dragScroll() {
|
|
if (dragging && insertionIndex !== undefined) {
|
|
calculateInsertionIndex(lastPointerX);
|
|
}
|
|
}
|
|
|
|
function endDrag() {
|
|
dragStartState = undefined;
|
|
dragging = false;
|
|
insertionIndex = undefined;
|
|
insertionMarkerLeft = undefined;
|
|
endCrossPanelDrag();
|
|
removeDragListeners();
|
|
}
|
|
|
|
function pointerIsInsideTabBar(e: PointerEvent): boolean {
|
|
const groupDiv = tabGroupElement?.div?.();
|
|
if (!groupDiv) return false;
|
|
|
|
const rect = groupDiv.getBoundingClientRect();
|
|
return e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
|
|
}
|
|
|
|
/// Detect which zone the pointer is in: the nearest edge (by diagonal quadrant) if within the 25% border, or center if interior.
|
|
function detectDockingEdge(clientX: number, clientY: number, rect: DOMRect): DockingEdge {
|
|
const distLeft = clientX - rect.left;
|
|
const distRight = rect.right - clientX;
|
|
const distTop = clientY - rect.top;
|
|
const distBottom = rect.bottom - clientY;
|
|
const minDist = Math.min(distLeft, distRight, distTop, distBottom);
|
|
|
|
// If the nearest edge is beyond the 25% threshold, it's the center zone
|
|
const THRESHOLD = 0.25;
|
|
const edgeThresholdX = rect.width * THRESHOLD;
|
|
const edgeThresholdY = rect.height * THRESHOLD;
|
|
if (distLeft > edgeThresholdX && distRight > edgeThresholdX && distTop > edgeThresholdY && distBottom > edgeThresholdY) return "Center";
|
|
|
|
// Return whichever edge is closest (diagonal dividing lines between quadrants)
|
|
if (minDist === distLeft) return "Left";
|
|
if (minDist === distRight) return "Right";
|
|
if (minDist === distTop) return "Top";
|
|
return "Bottom";
|
|
}
|
|
|
|
// Calculate the insertion position for a foreign panel's tab bar
|
|
function calculateForeignInsertionIndex(pointerX: number, targetPanelId: string, tabBarDiv: HTMLDivElement) {
|
|
const tabBarRect = tabBarDiv.getBoundingClientRect();
|
|
const tabs = tabBarDiv.querySelectorAll(":scope > [data-tab]");
|
|
let bestIndex = 0;
|
|
let bestMarkerLeft = 0;
|
|
|
|
for (let i = 0; i < tabs.length; i++) {
|
|
const tabRect = tabs[i].getBoundingClientRect();
|
|
const tabCenter = tabRect.left + tabRect.width / 2;
|
|
|
|
if (pointerX > tabCenter) {
|
|
bestIndex = i + 1;
|
|
bestMarkerLeft = tabRect.right - tabBarRect.left;
|
|
} else {
|
|
bestMarkerLeft = tabRect.left - tabBarRect.left;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Must be at least 2px from the left so its left half doesn't get cut off along the left of the tab bar
|
|
updateCrossPanelHover(targetPanelId, bestIndex, Math.max(2, bestMarkerLeft));
|
|
}
|
|
|
|
function calculateInsertionIndex(pointerX: number) {
|
|
const groupDiv = tabGroupElement?.div?.();
|
|
if (!dragStartState || !groupDiv) return;
|
|
|
|
const groupRect = groupDiv.getBoundingClientRect();
|
|
let bestIndex = 0;
|
|
let bestMarkerLeft = 0;
|
|
|
|
// Walk through each tab to find the insertion point closest to the pointer
|
|
for (let i = 0; i < tabLabels.length; i++) {
|
|
const tabDiv = tabElements[i]?.div?.();
|
|
if (!tabDiv) continue;
|
|
|
|
const tabRect = tabDiv.getBoundingClientRect();
|
|
const tabMidpoint = tabRect.left + tabRect.width / 2;
|
|
|
|
if (pointerX > tabMidpoint) {
|
|
bestIndex = i + 1;
|
|
bestMarkerLeft = tabRect.right - groupRect.left;
|
|
} else {
|
|
bestIndex = i;
|
|
bestMarkerLeft = tabRect.left - groupRect.left;
|
|
break;
|
|
}
|
|
}
|
|
|
|
insertionIndex = bestIndex;
|
|
insertionMarkerLeft = Math.max(2, bestMarkerLeft);
|
|
}
|
|
|
|
function addDragListeners() {
|
|
document.addEventListener("pointermove", dragPointerMove);
|
|
document.addEventListener("pointerup", dragPointerUp);
|
|
document.addEventListener("mousedown", dragAbort);
|
|
document.addEventListener("keydown", dragAbort);
|
|
tabGroupElement?.div?.()?.addEventListener("scroll", dragScroll);
|
|
}
|
|
|
|
function removeDragListeners() {
|
|
document.removeEventListener("pointermove", dragPointerMove);
|
|
document.removeEventListener("pointerup", dragPointerUp);
|
|
document.removeEventListener("mousedown", dragAbort);
|
|
document.removeEventListener("keydown", dragAbort);
|
|
tabGroupElement?.div?.()?.removeEventListener("scroll", dragScroll);
|
|
}
|
|
</script>
|
|
|
|
<LayoutCol
|
|
on:pointerdown={() => panelTypes[tabActiveIndex] && editor.setActivePanel(panelTypes[tabActiveIndex])}
|
|
class={`panel ${className}`.trim()}
|
|
{classes}
|
|
style={styleName}
|
|
{styles}
|
|
data-panel-body={panelId}
|
|
data-panel-dockable={crossPanelDropAction ? "" : undefined}
|
|
>
|
|
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }}>
|
|
<LayoutRow
|
|
class="tab-group"
|
|
scrollableX={true}
|
|
data-panel-tab-bar={crossPanelDropAction ? panelId : undefined}
|
|
on:pointerdown={tabBarPointerDown}
|
|
on:click={onEmptySpaceAction}
|
|
on:auxclick={onEmptySpaceAction}
|
|
bind:this={tabGroupElement}
|
|
>
|
|
{#each tabLabels as tabLabel, tabIndex}
|
|
<LayoutRow
|
|
class="tab"
|
|
classes={{ active: tabIndex === tabActiveIndex }}
|
|
data-tab
|
|
tooltipLabel={tabLabel.tooltipLabel}
|
|
tooltipDescription={tabLabel.tooltipDescription}
|
|
on:pointerdown={(e) => tabPointerDown(e, tabIndex)}
|
|
on:click={(e) => e.stopPropagation()}
|
|
on:auxclick={(e) => {
|
|
// Middle mouse button click
|
|
if (e.button === BUTTON_MIDDLE) {
|
|
e.stopPropagation();
|
|
closeAction?.(tabIndex);
|
|
}
|
|
}}
|
|
bind:this={tabElements[tabIndex]}
|
|
>
|
|
<LayoutRow class="name">
|
|
{#if editingNameTabIndex !== tabIndex}
|
|
<TextLabel class="text" on:dblclick={() => renameAction && setEditingTabName(tabIndex, tabLabel.name)}>{tabLabel.name}</TextLabel>
|
|
{:else}
|
|
<input
|
|
type="text"
|
|
bind:this={editingNameInputElement}
|
|
bind:value={editingNameText}
|
|
on:pointerdown|stopPropagation
|
|
on:dblclick|stopPropagation
|
|
on:blur={commitEditingTabName}
|
|
on:keydown={(e) => {
|
|
// Stop propagation when we handle the key ourselves so the global keyboard forwarder doesn't dispatch them and trigger unrelated bindings
|
|
if (e.key === "Enter") {
|
|
commitEditingTabName(e);
|
|
e.stopPropagation();
|
|
} else if (e.key === "Escape") {
|
|
editingNameTabIndex = undefined;
|
|
e.stopPropagation();
|
|
}
|
|
}}
|
|
/>
|
|
{/if}
|
|
{#if tabLabel.unsaved}
|
|
<TextLabel classes={{ hidden: editingNameTabIndex === tabIndex }}>*</TextLabel>
|
|
{/if}
|
|
</LayoutRow>
|
|
{#if tabCloseButtons}
|
|
<IconButton
|
|
action={(e) => {
|
|
e?.stopPropagation();
|
|
closeAction?.(tabIndex);
|
|
}}
|
|
icon="CloseX"
|
|
size={16}
|
|
data-close-button
|
|
/>
|
|
{/if}
|
|
</LayoutRow>
|
|
{/each}
|
|
</LayoutRow>
|
|
{#if dragging && insertionMarkerLeft !== undefined}
|
|
<div class="tab-insertion-mark" style:left={`${insertionMarkerLeft}px`}></div>
|
|
{/if}
|
|
{#if !dragging && crossPanelDropAction && $panelDrag.active && $panelDrag.hoverTargetPanelId === panelId && $panelDrag.hoverInsertionMarkerLeft !== undefined}
|
|
<div class="tab-insertion-mark" style:left={`${$panelDrag.hoverInsertionMarkerLeft}px`}></div>
|
|
{/if}
|
|
</LayoutRow>
|
|
<LayoutCol class="panel-body">
|
|
{#if panelTypes[tabActiveIndex]}
|
|
<svelte:component this={PANEL_COMPONENTS[panelTypes[tabActiveIndex]]} />
|
|
{/if}
|
|
</LayoutCol>
|
|
{#if $panelDrag.active && $panelDrag.hoverDockingPanelId === panelId && $panelDrag.hoverDockingEdge}
|
|
<div
|
|
class="docking-ghost"
|
|
class:left={$panelDrag.hoverDockingEdge === "Left"}
|
|
class:right={$panelDrag.hoverDockingEdge === "Right"}
|
|
class:top={$panelDrag.hoverDockingEdge === "Top"}
|
|
class:bottom={$panelDrag.hoverDockingEdge === "Bottom"}
|
|
class:center={$panelDrag.hoverDockingEdge === "Center"}
|
|
></div>
|
|
{/if}
|
|
</LayoutCol>
|
|
|
|
<style lang="scss">
|
|
.panel {
|
|
background: var(--color-1-nearblack);
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
|
|
.tab-bar {
|
|
position: relative;
|
|
height: 28px;
|
|
min-height: auto;
|
|
background: var(--color-1-nearblack); // Needed for the viewport hole punch on desktop
|
|
flex-shrink: 0;
|
|
|
|
&.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;
|
|
}
|
|
|
|
.tab {
|
|
flex: 0 1 auto;
|
|
height: 28px;
|
|
padding: 0 8px;
|
|
align-items: center;
|
|
position: relative;
|
|
|
|
&.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);
|
|
}
|
|
}
|
|
|
|
.name {
|
|
flex: 1 1 100%;
|
|
|
|
.text-label {
|
|
// Height and line-height required because https://stackoverflow.com/a/21611191/775283
|
|
height: 28px;
|
|
line-height: 28px;
|
|
flex: 0 0 auto;
|
|
|
|
&.text {
|
|
overflow-x: hidden;
|
|
white-space: pre;
|
|
text-overflow: ellipsis;
|
|
flex-shrink: 1;
|
|
}
|
|
}
|
|
|
|
input {
|
|
color: inherit;
|
|
border: none;
|
|
outline: none;
|
|
margin: 0 -4px;
|
|
padding: 0 4px;
|
|
height: 20px;
|
|
border-radius: 2px;
|
|
background: var(--color-1-nearblack);
|
|
field-sizing: content;
|
|
align-self: center;
|
|
// Stack above the absolutely-positioned close button so it doesn't intercept clicks at the input's right edge.
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.text-label.hidden {
|
|
visibility: hidden;
|
|
}
|
|
}
|
|
|
|
.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-5-dullgray);
|
|
}
|
|
|
|
&:last-of-type {
|
|
margin-right: 1px;
|
|
|
|
&:not(.active)::after {
|
|
content: "";
|
|
position: absolute;
|
|
right: -1px;
|
|
width: 1px;
|
|
height: 16px;
|
|
background: var(--color-5-dullgray);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
&:has(.tab-insertion-mark) .tab .icon-button {
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tab-insertion-mark {
|
|
position: absolute;
|
|
top: 4px;
|
|
bottom: 4px;
|
|
width: 3px;
|
|
margin-left: -2px;
|
|
z-index: 1;
|
|
background: var(--color-e-nearwhite);
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
|
|
.panel-body {
|
|
background: var(--color-3-darkgray);
|
|
flex: 1 1 100%;
|
|
flex-direction: column;
|
|
|
|
> div {
|
|
padding-bottom: 4px;
|
|
}
|
|
}
|
|
|
|
&:has(.docking-ghost) .tab-bar,
|
|
&:has(.docking-ghost) .panel-body {
|
|
pointer-events: none;
|
|
}
|
|
|
|
.docking-ghost {
|
|
position: absolute;
|
|
background: rgb(from var(--color-f-white) r g b / 0.2);
|
|
border-radius: 6px;
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
transition:
|
|
top 0.2s ease,
|
|
left 0.2s ease,
|
|
width 0.2s ease,
|
|
height 0.2s ease;
|
|
|
|
&.left {
|
|
top: 0;
|
|
left: 0;
|
|
width: 50%;
|
|
height: 100%;
|
|
}
|
|
|
|
&.right {
|
|
top: 0;
|
|
left: 50%;
|
|
width: 50%;
|
|
height: 100%;
|
|
}
|
|
|
|
&.top {
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 50%;
|
|
}
|
|
|
|
&.bottom {
|
|
top: 50%;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 50%;
|
|
}
|
|
|
|
&.center {
|
|
top: 6px;
|
|
left: 6px;
|
|
width: calc(100% - 12px);
|
|
height: calc(100% - 12px);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Needed for the viewport hole punch on desktop
|
|
.viewport-hole-punch .panel.document-panel,
|
|
.viewport-hole-punch .panel.document-panel .panel-body:not(:has(.welcome-panel)) {
|
|
background: none;
|
|
}
|
|
</style>
|