mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
create blockframes to replace blockheader (#59)
created two frames -- frameless and tech. frameless is used when there is 0 or 1 blocks, otherwise tech is used.
This commit is contained in:
parent
9ff8cb0292
commit
15681ffa1a
@ -37,6 +37,71 @@
|
||||
max-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&.block-frame-tech {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
margin: 10px 2px 2px 2px;
|
||||
padding: 10px 2px 2px 2px;
|
||||
height: calc(100% - 12px);
|
||||
width: calc(100% - 4px);
|
||||
overflow: visible;
|
||||
|
||||
&.block-preview {
|
||||
background-color: var(--main-bg-color);
|
||||
}
|
||||
|
||||
&.block-focused {
|
||||
border: 2px solid var(--accent-color);
|
||||
|
||||
.block-frame-tech-header {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.block-frame-tech-close {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.block-frame-tech-header {
|
||||
position: absolute;
|
||||
max-width: 85%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
top: -11px;
|
||||
padding: 4px 6px 4px 6px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--main-bg-color);
|
||||
font: var(--fixed-font);
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.block-frame-tech-close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -2px;
|
||||
padding: 0 0 1px 1px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--panel-bg-color);
|
||||
cursor: pointer;
|
||||
color: var(--grey-text-color);
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.block-frame-preview {
|
||||
background-color: var(--main-bg-color);
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.block-header {
|
||||
@ -61,6 +126,7 @@
|
||||
.block-header-text {
|
||||
padding: 0 5px;
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
|
@ -7,8 +7,10 @@ import { PreviewView } from "@/app/view/preview";
|
||||
import { TerminalView } from "@/app/view/term/term";
|
||||
import { ErrorBoundary } from "@/element/errorboundary";
|
||||
import { CenteredDiv } from "@/element/quickelems";
|
||||
import { atoms, useBlockAtom } from "@/store/global";
|
||||
import * as WOS from "@/store/wos";
|
||||
import clsx from "clsx";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
|
||||
import "./block.less";
|
||||
@ -21,14 +23,19 @@ interface BlockProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const BlockHeader = ({ blockId, onClose }: BlockProps) => {
|
||||
function getBlockHeaderText(blockData: Block): string {
|
||||
if (!blockData) {
|
||||
return "no block data";
|
||||
}
|
||||
return `${blockData?.view} [${blockData.oid.substring(0, 8)}]`;
|
||||
}
|
||||
|
||||
const FramelessBlockHeader = ({ blockId, onClose }: BlockProps) => {
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
|
||||
return (
|
||||
<div key="header" className="block-header">
|
||||
<div className="block-header-text text-fixed">
|
||||
Block [{blockId.substring(0, 8)}] {blockData?.view}
|
||||
</div>
|
||||
<div className="block-header-text text-fixed">{getBlockHeaderText(blockData)}</div>
|
||||
{onClose && (
|
||||
<div className="close-button" onClick={onClose}>
|
||||
<i className="fa fa-solid fa-xmark-large" />
|
||||
@ -42,12 +49,52 @@ const hoverStateOff = "off";
|
||||
const hoverStatePending = "pending";
|
||||
const hoverStateOn = "on";
|
||||
|
||||
const Block = ({ blockId, onClose }: BlockProps) => {
|
||||
interface BlockFrameProps {
|
||||
blockId: string;
|
||||
onClose?: () => void;
|
||||
preview: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const BlockFrame_Tech = ({ blockId, onClose, preview, children }: BlockFrameProps) => {
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
|
||||
return jotai.atom((get) => {
|
||||
const winData = get(atoms.waveWindow);
|
||||
return winData.activeblockid === blockId;
|
||||
});
|
||||
});
|
||||
let isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||
if (preview) {
|
||||
isFocused = true;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"block",
|
||||
"block-frame-tech",
|
||||
isFocused ? "block-focused" : null,
|
||||
preview ? "block-preview" : null
|
||||
)}
|
||||
>
|
||||
<div className="block-frame-tech-header">{getBlockHeaderText(blockData)}</div>
|
||||
<div className="block-frame-tech-close" onClick={onClose}>
|
||||
<i className="fa fa-solid fa-xmark fa-fw " />
|
||||
</div>
|
||||
{preview ? <div className="block-frame-preview" /> : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockFrame_Frameless = ({ blockId, onClose, preview, children }: BlockFrameProps) => {
|
||||
const blockRef = React.useRef<HTMLDivElement>(null);
|
||||
const [showHeader, setShowHeader] = React.useState(preview ? true : false);
|
||||
const hoverState = React.useRef(hoverStateOff);
|
||||
const [showHeader, setShowHeader] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (preview) {
|
||||
return;
|
||||
}
|
||||
const block = blockRef.current;
|
||||
let hoverTimeout: NodeJS.Timeout = null;
|
||||
const handleMouseMove = (event) => {
|
||||
@ -77,7 +124,43 @@ const Block = ({ blockId, onClose }: BlockProps) => {
|
||||
block.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
});
|
||||
let mouseLeaveHandler = () => {
|
||||
if (preview) {
|
||||
return;
|
||||
}
|
||||
setShowHeader(false);
|
||||
hoverState.current = hoverStateOff;
|
||||
};
|
||||
return (
|
||||
<div className="block block-frame-frameless" ref={blockRef} onMouseLeave={mouseLeaveHandler}>
|
||||
<div
|
||||
className={clsx("block-header-animation-wrap", showHeader ? "is-showing" : null)}
|
||||
onMouseLeave={mouseLeaveHandler}
|
||||
>
|
||||
<FramelessBlockHeader blockId={blockId} onClose={onClose} />
|
||||
</div>
|
||||
{preview ? <div className="block-frame-preview" /> : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockFrame = (props: BlockFrameProps) => {
|
||||
const blockId = props.blockId;
|
||||
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
const tabData = jotai.useAtomValue(atoms.tabAtom);
|
||||
|
||||
if (!blockId || !blockData) {
|
||||
return null;
|
||||
}
|
||||
// if 0 or 1 blocks, use frameless, otherwise use tech
|
||||
const numBlocks = tabData?.blockids?.length ?? 0;
|
||||
if (numBlocks <= 1) {
|
||||
return <BlockFrame_Frameless {...props} />;
|
||||
}
|
||||
return <BlockFrame_Tech {...props} />;
|
||||
};
|
||||
|
||||
const Block = ({ blockId, onClose }: BlockProps) => {
|
||||
let blockElem: JSX.Element = null;
|
||||
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
if (!blockId || !blockData) return null;
|
||||
@ -93,30 +176,14 @@ const Block = ({ blockId, onClose }: BlockProps) => {
|
||||
blockElem = <CodeEdit text={null} filename={null} />;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="block"
|
||||
ref={blockRef}
|
||||
onMouseLeave={() => {
|
||||
setShowHeader(false);
|
||||
hoverState.current = hoverStateOff;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clsx("block-header-animation-wrap", showHeader ? "is-showing" : null)}
|
||||
onMouseLeave={() => {
|
||||
setShowHeader(false);
|
||||
hoverState.current = hoverStateOff;
|
||||
}}
|
||||
>
|
||||
<BlockHeader blockId={blockId} onClose={onClose} />
|
||||
</div>
|
||||
<BlockFrame blockId={blockId} onClose={onClose} preview={false}>
|
||||
<div key="content" className="block-content">
|
||||
<ErrorBoundary>
|
||||
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{blockElem}</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</BlockFrame>
|
||||
);
|
||||
};
|
||||
|
||||
export { Block, BlockHeader };
|
||||
export { Block, BlockFrame };
|
||||
|
@ -56,6 +56,13 @@ const workspaceAtom: jotai.Atom<Workspace> = jotai.atom((get) => {
|
||||
}
|
||||
return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get);
|
||||
});
|
||||
const tabAtom: jotai.Atom<Tab> = jotai.atom((get) => {
|
||||
const windowData = get(windowDataAtom);
|
||||
if (windowData == null) {
|
||||
return null;
|
||||
}
|
||||
return WOS.getObjectValue(WOS.makeORef("tab", windowData.activetabid), get);
|
||||
});
|
||||
|
||||
const atoms = {
|
||||
// initialized in wave.ts (will not be null inside of application)
|
||||
@ -65,6 +72,7 @@ const atoms = {
|
||||
client: clientAtom,
|
||||
waveWindow: windowDataAtom,
|
||||
workspace: workspaceAtom,
|
||||
tabAtom: tabAtom,
|
||||
};
|
||||
|
||||
// key is "eventType" or "eventType|oref"
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Block, BlockHeader } from "@/app/block/block";
|
||||
import { Block, BlockFrame } from "@/app/block/block";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
|
||||
@ -30,7 +30,7 @@ const TabContent = ({ tabId }: { tabId: string }) => {
|
||||
|
||||
const renderPreview = useCallback((tabData: TabLayoutData) => {
|
||||
console.log("renderPreview", tabData);
|
||||
return <BlockHeader blockId={tabData.blockId} />;
|
||||
return <BlockFrame blockId={tabData.blockId} preview={true} />;
|
||||
}, []);
|
||||
|
||||
const onNodeDelete = useCallback((data: TabLayoutData) => {
|
||||
|
@ -5,6 +5,7 @@
|
||||
--main-text-color: #f7f7f7;
|
||||
--title-font-size: 18px;
|
||||
--secondary-text-color: rgb(195, 200, 194);
|
||||
--grey-text-color: #666;
|
||||
--main-bg-color: #000000;
|
||||
--border-color: #333333;
|
||||
--base-font: normal 15px / normal "Lato", sans-serif;
|
||||
|
@ -11,10 +11,6 @@
|
||||
padding-left: 4px;
|
||||
position: relative;
|
||||
|
||||
&:focus-within {
|
||||
border-left: 4px solid var(--accent-color);
|
||||
}
|
||||
|
||||
.term-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import clsx from "clsx";
|
||||
import { toPng } from "html-to-image";
|
||||
import React, {
|
||||
CSSProperties,
|
||||
ReactNode,
|
||||
@ -17,7 +18,6 @@ import React, {
|
||||
import { useDrag, useDragLayer, useDrop } from "react-dnd";
|
||||
|
||||
import useResizeObserver from "@react-hook/resize-observer";
|
||||
import { toPng } from "html-to-image";
|
||||
import { useLayoutTreeStateReducerAtom } from "./layoutAtom";
|
||||
import { findNode } from "./layoutNode";
|
||||
import {
|
||||
@ -60,6 +60,9 @@ export interface TileLayoutProps<T> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DragPreviewWidth = 300;
|
||||
const DragPreviewHeight = 300;
|
||||
|
||||
export const TileLayout = <T,>({
|
||||
layoutTreeStateAtom,
|
||||
className,
|
||||
@ -289,6 +292,7 @@ const DisplayNode = <T,>({
|
||||
}: DisplayNodeProps<T>) => {
|
||||
const tileNodeRef = useRef<HTMLDivElement>(null);
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
const hasImagePreviewSetRef = useRef(false);
|
||||
|
||||
// Register the node as a draggable item.
|
||||
const [{ isDragging }, drag, dragPreview] = useDrag(
|
||||
@ -302,32 +306,32 @@ const DisplayNode = <T,>({
|
||||
[layoutNode]
|
||||
);
|
||||
|
||||
// Generate a preview div using the provided renderPreview function. This will be placed in the DOM so we can render an image from it, but it is pushed out of view so the user will not see it.
|
||||
// No-op if not provided, meaning React-DnD will attempt to generate a preview from the DOM, which is very slow.
|
||||
const preview = useMemo(() => {
|
||||
const previewElement = renderPreview?.(layoutNode.data);
|
||||
return (
|
||||
<div className="tile-preview-container">
|
||||
<div className="tile-preview" ref={previewRef}>
|
||||
{previewElement}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const previewWidth = DragPreviewWidth;
|
||||
const previewHeight = DragPreviewHeight;
|
||||
const previewTransform = `scale(${1 / window.devicePixelRatio})`;
|
||||
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
|
||||
// we set the drag preview on load to be the HTML element
|
||||
// later, on pointerenter, we generate a static png preview to use instead (for performance)
|
||||
useEffect(() => {
|
||||
if (!hasImagePreviewSetRef.current) {
|
||||
dragPreview(previewRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Cache the preview image after we generate it
|
||||
const [previewImage, setPreviewImage] = useState<HTMLImageElement>();
|
||||
|
||||
// When a user first mouses over a node, generate a preview image and set it as the drag preview.
|
||||
const generatePreviewImage = useCallback(() => {
|
||||
if (previewImage) {
|
||||
dragPreview(previewImage);
|
||||
let offsetX = (DragPreviewWidth * window.devicePixelRatio - DragPreviewWidth) / 2 + 10;
|
||||
let offsetY = (DragPreviewHeight * window.devicePixelRatio - DragPreviewHeight) / 2 + 10;
|
||||
if (previewImage != null) {
|
||||
dragPreview(previewImage, { offsetY, offsetX });
|
||||
} else if (previewRef.current) {
|
||||
toPng(previewRef.current).then((url) => {
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
img.onload = () => dragPreview(img);
|
||||
img.onload = () => {
|
||||
hasImagePreviewSetRef.current = true;
|
||||
setPreviewImage(img);
|
||||
dragPreview(img, { offsetY, offsetX });
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [previewRef, previewImage, dragPreview]);
|
||||
@ -364,7 +368,19 @@ const DisplayNode = <T,>({
|
||||
onPointerEnter={generatePreviewImage}
|
||||
>
|
||||
{leafContent}
|
||||
{preview}
|
||||
<div key="preview" className="tile-preview-container">
|
||||
<div
|
||||
className="tile-preview"
|
||||
ref={previewRef}
|
||||
style={{
|
||||
width: previewWidth,
|
||||
height: previewHeight,
|
||||
transform: previewTransform,
|
||||
}}
|
||||
>
|
||||
{previewElement}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -46,13 +46,16 @@
|
||||
}
|
||||
|
||||
.tile-preview-container {
|
||||
height: fit-content !important;
|
||||
width: fit-content !important;
|
||||
position: absolute;
|
||||
top: 10000px;
|
||||
white-space: nowrap !important;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
.tile-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,6 +78,7 @@
|
||||
|
||||
.tile-leaf {
|
||||
border: 1px solid black;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
|
Loading…
Reference in New Issue
Block a user