Add action for magnifying a block (#172)

Adds the implementation for the "Magnify Block" context menu item. This
will pop a block out of the layout and bring it to the foreground.

This also cleans up some block styling to make radii more consistent.

<img width="814" alt="image"
src="https://github.com/user-attachments/assets/c81521e1-c91f-4bb5-9eec-ff0eda178268">
This commit is contained in:
Evan Simkowitz 2024-07-30 10:59:53 -07:00 committed by GitHub
parent 49072364e9
commit f3f272a47b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 295 additions and 156 deletions

View File

@ -11,7 +11,7 @@
overflow: hidden; overflow: hidden;
min-height: 0; min-height: 0;
position: relative; position: relative;
border-radius: 8px; border-radius: var(--block-border-radius);
.block-frame-icon { .block-frame-icon {
margin-right: 0.5em; margin-right: 0.5em;
@ -62,7 +62,7 @@
background-color: var(--block-bg-color); background-color: var(--block-bg-color);
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 8px; border-radius: var(--block-border-radius);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -75,7 +75,7 @@
align-self: stretch; align-self: stretch;
font: var(--header-font); font: var(--header-font);
border-bottom: 1px solid rgba(255, 255, 255, 0.08); border-bottom: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px 8px 0 0; border-radius: var(--block-border-radius) var(--block-border-radius) 0 0;
.block-frame-preicon-button { .block-frame-preicon-button {
opacity: 0.7; opacity: 0.7;
@ -251,8 +251,8 @@
background-color: rgba(0, 0, 0, 0.7); background-color: rgba(0, 0, 0, 0.7);
width: 100%; width: 100%;
flex-grow: 1; flex-grow: 1;
border-bottom-left-radius: 8px; border-bottom-left-radius: var(--block-border-radius);
border-bottom-right-radius: 8px; border-bottom-right-radius: var(--block-border-radius);
} }
} }
@ -265,7 +265,7 @@
border: 2px solid transparent; border: 2px solid transparent;
pointer-events: none; pointer-events: none;
padding: 2px; padding: 2px;
border-radius: 8px; border-radius: var(--block-border-radius);
} }
&.block-focused { &.block-focused {

View File

@ -21,8 +21,9 @@ import * as React from "react";
import "./block.less"; import "./block.less";
interface LayoutComponentModel { export interface LayoutComponentModel {
onClose?: () => void; onClose?: () => void;
onMagnifyToggle?: () => void;
dragHandleRef?: React.RefObject<HTMLDivElement>; dragHandleRef?: React.RefObject<HTMLDivElement>;
} }
@ -163,51 +164,52 @@ function handleHeaderContextMenu(
e: React.MouseEvent<HTMLDivElement>, e: React.MouseEvent<HTMLDivElement>,
blockData: Block, blockData: Block,
viewModel: ViewModel, viewModel: ViewModel,
onMagnifyToggle: () => void,
onClose: () => void onClose: () => void
) { ) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
let menu: ContextMenuItem[] = []; let menu: ContextMenuItem[] = [
menu.push({ {
label: "Focus Block", label: "Magnify Block",
click: () => { click: () => {
alert("Not Implemented"); onMagnifyToggle();
}, },
}); },
menu.push({ {
label: "Minimize", label: "Minimize",
click: () => { click: () => {
alert("Not Implemented"); alert("Not Implemented");
}, },
}); },
menu.push({ {
label: "Move to New Window", label: "Move to New Window",
click: () => { click: () => {
let currentTabId = globalStore.get(atoms.activeTabId); const currentTabId = globalStore.get(atoms.activeTabId);
try { try {
services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid); services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
} catch (e) { } catch (e) {
console.error("error moving block to new window", e); console.error("error moving block to new window", e);
} }
}, },
}); },
menu.push({ type: "separator" }); { type: "separator" },
menu.push({ {
label: "Copy BlockId", label: "Copy BlockId",
click: () => { click: () => {
navigator.clipboard.writeText(blockData.oid); navigator.clipboard.writeText(blockData.oid);
}, },
}); },
];
const extraItems = viewModel?.getSettingsMenuItems?.(); const extraItems = viewModel?.getSettingsMenuItems?.();
if (extraItems && extraItems.length > 0) { if (extraItems && extraItems.length > 0) menu.push({ type: "separator" }, ...extraItems);
menu.push({ type: "separator" }); menu.push(
menu.push(...extraItems); { type: "separator" },
} {
menu.push({ type: "separator" });
menu.push({
label: "Close Block", label: "Close Block",
click: onClose, click: onClose,
}); }
);
ContextMenuModel.showContextMenu(menu, e); ContextMenuModel.showContextMenu(menu, e);
} }
@ -294,7 +296,8 @@ const BlockFrame_Default_Component = ({
elemtype: "iconbutton", elemtype: "iconbutton",
icon: "cog", icon: "cog",
title: "Settings", title: "Settings",
click: (e) => handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onClose), click: (e) =>
handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onMagnifyToggle, layoutModel?.onClose),
}; };
endIconsElem.push( endIconsElem.push(
<IconButton key="settings" decl={settingsDecl} className="block-frame-endicon-button block-frame-settings" /> <IconButton key="settings" decl={settingsDecl} className="block-frame-endicon-button block-frame-settings" />
@ -394,7 +397,15 @@ const BlockFrame_Default_Component = ({
<div <div
className="block-frame-default-header" className="block-frame-default-header"
ref={layoutModel?.dragHandleRef} ref={layoutModel?.dragHandleRef}
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onClose)} onContextMenu={(e) =>
handleHeaderContextMenu(
e,
blockData,
viewModel,
layoutModel?.onMagnifyToggle,
layoutModel?.onClose
)
}
> >
{preIconButtonElem} {preIconButtonElem}
<div className="block-frame-default-header-iconview"> <div className="block-frame-default-header-iconview">

View File

@ -1,7 +1,7 @@
// Copyright 2023, Command Line Inc. // Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Block } from "@/app/block/block"; import { Block, LayoutComponentModel } from "@/app/block/block";
import { getApi } from "@/store/global"; import { getApi } from "@/store/global";
import * as services from "@/store/services"; import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
@ -26,14 +26,16 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => {
function renderBlock( function renderBlock(
tabData: TabLayoutData, tabData: TabLayoutData,
ready: boolean, ready: boolean,
onMagnifyToggle: () => void,
onClose: () => void, onClose: () => void,
dragHandleRef: React.RefObject<HTMLDivElement> dragHandleRef: React.RefObject<HTMLDivElement>
) { ) {
if (!tabData.blockId || !ready) { if (!tabData.blockId || !ready) {
return null; return null;
} }
const layoutModel = { const layoutModel: LayoutComponentModel = {
onClose: onClose, onClose: onClose,
onMagnifyToggle: onMagnifyToggle,
dragHandleRef: dragHandleRef, dragHandleRef: dragHandleRef,
}; };
return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={layoutModel} preview={false} />; return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={layoutModel} preview={false} />;

View File

@ -20,6 +20,8 @@
--success-color: rgb(78, 154, 6); --success-color: rgb(78, 154, 6);
--hover-bg-color: rgba(255, 255, 255, 0.1); --hover-bg-color: rgba(255, 255, 255, 0.1);
--block-bg-color: rgba(0, 0, 0, 0.5); --block-bg-color: rgba(0, 0, 0, 0.5);
--block-bg-solid-color: rgb(0, 0, 0);
--block-border-radius: 8px;
/* scrollbar colors */ /* scrollbar colors */
--scrollbar-background-color: transparent; --scrollbar-background-color: transparent;

View File

@ -40,7 +40,7 @@ import {
} from "./model"; } from "./model";
import { NodeRefMap } from "./nodeRefMap"; import { NodeRefMap } from "./nodeRefMap";
import "./tilelayout.less"; import "./tilelayout.less";
import { Dimensions, FlexDirection, setTransform as createTransform, determineDropDirection } from "./utils"; import { Dimensions, FlexDirection, determineDropDirection, setTransform } from "./utils";
/** /**
* contains callbacks and information about the contents (or styling) of of the TileLayout * contains callbacks and information about the contents (or styling) of of the TileLayout
@ -181,14 +181,28 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
const leafRef = nodeRefs.get(leaf.id); const leafRef = nodeRefs.get(leaf.id);
// console.log("current leafRef", leafRef.current); // console.log("current leafRef", leafRef.current);
if (leafRef?.current) { if (leafRef?.current) {
if (leaf.id === layoutTreeState.magnifiedNodeId) {
const transform = setTransform(
{
top: displayBoundingRect.height * 0.05,
left: displayBoundingRect.width * 0.05,
width: displayBoundingRect.width * 0.9,
height: displayBoundingRect.height * 0.9,
},
true
);
newLayoutLeafTransforms[leaf.id] = transform;
continue;
}
const leafBounding = leafRef.current.getBoundingClientRect(); const leafBounding = leafRef.current.getBoundingClientRect();
const transform = createTransform({ const transform = setTransform({
top: leafBounding.top - overlayBoundingRect.top, top: leafBounding.top - overlayBoundingRect.top,
left: leafBounding.left - overlayBoundingRect.left, left: leafBounding.left - overlayBoundingRect.left,
width: leafBounding.width, width: leafBounding.width,
height: leafBounding.height, height: leafBounding.height,
}); });
newLayoutLeafTransforms[leafRef.current.id] = transform; newLayoutLeafTransforms[leaf.id] = transform;
} else { } else {
console.warn("missing leaf", leaf.id); console.warn("missing leaf", leaf.id);
} }
@ -199,7 +213,7 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
const newOverlayOffset = displayBoundingRect.top + 2 * displayBoundingRect.height; const newOverlayOffset = displayBoundingRect.top + 2 * displayBoundingRect.height;
// console.log("overlayOffset", newOverlayOffset); // console.log("overlayOffset", newOverlayOffset);
setOverlayTransform( setOverlayTransform(
createTransform( setTransform(
{ {
top: activeDrag || showOverlay ? 0 : newOverlayOffset, top: activeDrag || showOverlay ? 0 : newOverlayOffset,
left: 0, left: 0,
@ -246,6 +260,18 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
[contents.onNodeDelete, dispatch] [contents.onNodeDelete, dispatch]
); );
const onLeafMagnifyToggle = useCallback(
(node: LayoutNode<T>) => {
const action = {
type: LayoutTreeActionType.MagnifyNodeToggle,
nodeId: node.id,
};
dispatch(action);
},
[dispatch]
);
return ( return (
<Suspense> <Suspense>
<div className={clsx("tile-layout", contents.className, { animate })} onPointerOver={onPointerOver}> <div className={clsx("tile-layout", contents.className, { animate })} onPointerOver={onPointerOver}>
@ -253,6 +279,7 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
<DisplayNodesWrapper <DisplayNodesWrapper
contents={contents} contents={contents}
ready={animate} ready={animate}
onLeafMagnifyToggle={onLeafMagnifyToggle}
onLeafClose={onLeafClose} onLeafClose={onLeafClose}
layoutTreeState={layoutTreeState} layoutTreeState={layoutTreeState}
layoutLeafTransforms={layoutLeafTransforms} layoutLeafTransforms={layoutLeafTransforms}
@ -303,6 +330,13 @@ interface DisplayNodesWrapperProps<T> {
* @param node The node that is closed. * @param node The node that is closed.
*/ */
onLeafClose: (node: LayoutNode<T>) => void; onLeafClose: (node: LayoutNode<T>) => void;
/**
* A callback that is called when a leaf's magnification is being toggled.
* @param node The node that is being magnified or un-magnified.
*/
onLeafMagnifyToggle: (node: LayoutNode<T>) => void;
/** /**
* A series of CSS properties used to display a leaf node with the correct dimensions and position, as determined from its corresponding OverlayNode. * A series of CSS properties used to display a leaf node with the correct dimensions and position, as determined from its corresponding OverlayNode.
*/ */
@ -314,18 +348,27 @@ interface DisplayNodesWrapperProps<T> {
} }
const DisplayNodesWrapper = memo( const DisplayNodesWrapper = memo(
<T,>({ layoutTreeState, contents, onLeafClose, layoutLeafTransforms, ready }: DisplayNodesWrapperProps<T>) => { <T,>({
layoutTreeState,
contents,
onLeafClose,
onLeafMagnifyToggle,
layoutLeafTransforms,
ready,
}: DisplayNodesWrapperProps<T>) => {
if (!layoutLeafTransforms) { if (!layoutLeafTransforms) {
return null; return null;
} }
return layoutTreeState.leafs.map((leaf) => { return layoutTreeState.leafs.map((leaf) => {
return ( return (
<DisplayNode <DisplayNode
className={clsx({ magnified: layoutTreeState.magnifiedNodeId === leaf.id })}
key={leaf.id} key={leaf.id}
layoutNode={leaf} layoutNode={leaf}
contents={contents} contents={contents}
transform={layoutLeafTransforms[leaf.id]} transform={layoutLeafTransforms[leaf.id]}
onLeafClose={onLeafClose} onLeafClose={onLeafClose}
onLeafMagnifyToggle={onLeafMagnifyToggle}
ready={ready} ready={ready}
/> />
); );
@ -344,6 +387,12 @@ interface DisplayNodeProps<T> {
*/ */
contents: TileLayoutContents<T>; contents: TileLayoutContents<T>;
/**
* A callback that is called when a leaf's magnification is being toggled.
* @param node The node that is being magnified or unmagnified.
*/
onLeafMagnifyToggle: (node: LayoutNode<T>) => void;
/** /**
* A callback that is called when a leaf node gets closed. * A callback that is called when a leaf node gets closed.
* @param node The node that is closed. * @param node The node that is closed.
@ -353,6 +402,12 @@ interface DisplayNodeProps<T> {
* Determines whether a leaf's contents should be displayed to the user. * Determines whether a leaf's contents should be displayed to the user.
*/ */
ready: boolean; ready: boolean;
/**
* Any class names to add to the component.
*/
className?: string;
/** /**
* A series of CSS properties used to display a leaf node with the correct dimensions and position, as determined from its corresponding OverlayNode. * A series of CSS properties used to display a leaf node with the correct dimensions and position, as determined from its corresponding OverlayNode.
*/ */
@ -364,7 +419,16 @@ const dragItemType = "TILE_ITEM";
/** /**
* The draggable and displayable portion of a leaf node in a layout tree. * The draggable and displayable portion of a leaf node in a layout tree.
*/ */
const DisplayNode = memo(<T,>({ layoutNode, contents, transform, onLeafClose, ready }: DisplayNodeProps<T>) => { const DisplayNode = memo(
<T,>({
layoutNode,
contents,
transform,
onLeafMagnifyToggle,
onLeafClose,
ready,
className,
}: DisplayNodeProps<T>) => {
const tileNodeRef = useRef<HTMLDivElement>(null); const tileNodeRef = useRef<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null); const dragHandleRef = useRef<HTMLDivElement>(null);
const previewRef = useRef<HTMLDivElement>(null); const previewRef = useRef<HTMLDivElement>(null);
@ -436,11 +500,15 @@ const DisplayNode = memo(<T,>({ layoutNode, contents, transform, onLeafClose, re
onLeafClose(layoutNode); onLeafClose(layoutNode);
}, [layoutNode, onLeafClose]); }, [layoutNode, onLeafClose]);
const onMagnifyToggle = useCallback(() => {
onLeafMagnifyToggle(layoutNode);
}, [layoutNode, onLeafMagnifyToggle]);
const leafContent = useMemo(() => { const leafContent = useMemo(() => {
return ( return (
layoutNode.data && ( layoutNode.data && (
<div key="leaf" className="tile-leaf"> <div key="leaf" className="tile-leaf">
{contents.renderContent(layoutNode.data, ready, onClose, dragHandleRef)} {contents.renderContent(layoutNode.data, ready, onMagnifyToggle, onClose, dragHandleRef)}
</div> </div>
) )
); );
@ -448,7 +516,7 @@ const DisplayNode = memo(<T,>({ layoutNode, contents, transform, onLeafClose, re
return ( return (
<div <div
className={clsx("tile-node", { dragging: isDragging })} className={clsx("tile-node", className, { dragging: isDragging })}
ref={tileNodeRef} ref={tileNodeRef}
id={layoutNode.id} id={layoutNode.id}
style={{ style={{
@ -461,7 +529,8 @@ const DisplayNode = memo(<T,>({ layoutNode, contents, transform, onLeafClose, re
{previewElement} {previewElement}
</div> </div>
); );
}); }
);
interface OverlayNodeProps<T> { interface OverlayNodeProps<T> {
/** /**
@ -886,7 +955,7 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefsAtom, s
} }
} }
const placeholderTransform = createTransform(placeholderDimensions); const placeholderTransform = setTransform(placeholderDimensions);
newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />; newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />;
} }
} }
@ -907,7 +976,7 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefsAtom, s
width: targetBoundingRect.width, width: targetBoundingRect.width,
}; };
const placeholderTransform = createTransform(placeholderDimensions); const placeholderTransform = setTransform(placeholderDimensions);
newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />; newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />;
} }
break; break;

View File

@ -78,7 +78,7 @@ function getLayoutNodeWaveObjAtomFromTab<T>(
const tabValue = get(tabAtom); const tabValue = get(tabAtom);
// console.log("getLayoutNodeWaveObjAtomFromTab tabValue", tabValue); // console.log("getLayoutNodeWaveObjAtomFromTab tabValue", tabValue);
if (!tabValue) return; if (!tabValue) return;
const layoutNodeOref = WOS.makeORef("layout", tabValue.layoutNode); const layoutNodeOref = WOS.makeORef("layout", tabValue.layoutnode);
// console.log("getLayoutNodeWaveObjAtomFromTab oref", layoutNodeOref); // console.log("getLayoutNodeWaveObjAtomFromTab oref", layoutNodeOref);
return WOS.getWaveObjectAtom<LayoutNodeWaveObj<T>>(layoutNodeOref); return WOS.getWaveObjectAtom<LayoutNodeWaveObj<T>>(layoutNodeOref);
} }
@ -86,13 +86,16 @@ function getLayoutNodeWaveObjAtomFromTab<T>(
export function withLayoutStateAtomFromTab<T>(tabAtom: Atom<Tab>): WritableLayoutTreeStateAtom<T> { export function withLayoutStateAtomFromTab<T>(tabAtom: Atom<Tab>): WritableLayoutTreeStateAtom<T> {
const pendingActionAtom = atom<LayoutTreeAction>(null) as PrimitiveAtom<LayoutTreeAction>; const pendingActionAtom = atom<LayoutTreeAction>(null) as PrimitiveAtom<LayoutTreeAction>;
const generationAtom = atom(0) as PrimitiveAtom<number>; const generationAtom = atom(0) as PrimitiveAtom<number>;
return atom( return atom(
(get) => { (get) => {
const waveObjAtom = getLayoutNodeWaveObjAtomFromTab<T>(tabAtom, get); const waveObjAtom = getLayoutNodeWaveObjAtomFromTab<T>(tabAtom, get);
if (!waveObjAtom) return null; if (!waveObjAtom) return null;
const layoutState = newLayoutTreeState(get(waveObjAtom)?.node); const waveObj = get(waveObjAtom);
const layoutState = newLayoutTreeState(waveObj?.node);
layoutState.pendingAction = get(pendingActionAtom); layoutState.pendingAction = get(pendingActionAtom);
layoutState.generation = get(generationAtom); layoutState.generation = get(generationAtom);
layoutState.magnifiedNodeId = waveObj?.magnifiednodeid;
return layoutState; return layoutState;
}, },
(get, set, value) => { (get, set, value) => {
@ -100,7 +103,11 @@ export function withLayoutStateAtomFromTab<T>(tabAtom: Atom<Tab>): WritableLayou
if (get(generationAtom) !== value.generation) { if (get(generationAtom) !== value.generation) {
const waveObjAtom = getLayoutNodeWaveObjAtomFromTab<T>(tabAtom, get); const waveObjAtom = getLayoutNodeWaveObjAtomFromTab<T>(tabAtom, get);
if (!waveObjAtom) return; if (!waveObjAtom) return;
const newWaveObj = { ...get(waveObjAtom), node: value.rootNode }; const newWaveObj = {
...get(waveObjAtom),
node: value.rootNode,
magnifiednodeid: value.magnifiedNodeId,
};
set(generationAtom, value.generation); set(generationAtom, value.generation);
set(waveObjAtom, newWaveObj); set(waveObjAtom, newWaveObj);
} }

View File

@ -19,6 +19,7 @@ import {
LayoutTreeComputeMoveNodeAction, LayoutTreeComputeMoveNodeAction,
LayoutTreeDeleteNodeAction, LayoutTreeDeleteNodeAction,
LayoutTreeInsertNodeAction, LayoutTreeInsertNodeAction,
LayoutTreeMagnifyNodeToggleAction,
LayoutTreeMoveNodeAction, LayoutTreeMoveNodeAction,
LayoutTreeResizeNodeAction, LayoutTreeResizeNodeAction,
LayoutTreeSetPendingAction, LayoutTreeSetPendingAction,
@ -108,6 +109,10 @@ function layoutTreeStateReducerInner<T>(layoutTreeState: LayoutTreeState<T>, act
resizeNode(layoutTreeState, action as LayoutTreeResizeNodeAction); resizeNode(layoutTreeState, action as LayoutTreeResizeNodeAction);
layoutTreeState.generation++; layoutTreeState.generation++;
break; break;
case LayoutTreeActionType.MagnifyNodeToggle:
magnifyNodeToggle(layoutTreeState, action as LayoutTreeMagnifyNodeToggleAction);
layoutTreeState.generation++;
break;
default: { default: {
console.error("Invalid reducer action", layoutTreeState, action); console.error("Invalid reducer action", layoutTreeState, action);
} }
@ -460,3 +465,20 @@ function resizeNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeRe
node.size = resize.size; node.size = resize.size;
} }
} }
function magnifyNodeToggle<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMagnifyNodeToggleAction) {
console.log("magnifyNodeToggle", layoutTreeState, action);
if (!action.nodeId) {
console.error("invalid magnifyNodeToggle operation. nodeId must be defined.");
return;
}
if (layoutTreeState.rootNode.id === action.nodeId) {
console.warn(`cannot toggle magnification of node ${action.nodeId} because it is the root node.`);
return;
}
if (layoutTreeState.magnifiedNodeId === action.nodeId) {
layoutTreeState.magnifiedNodeId = undefined;
} else {
layoutTreeState.magnifiedNodeId = action.nodeId;
}
}

View File

@ -42,6 +42,7 @@ export enum LayoutTreeActionType {
ResizeNode = "resize", ResizeNode = "resize",
InsertNode = "insert", InsertNode = "insert",
DeleteNode = "delete", DeleteNode = "delete",
MagnifyNodeToggle = "magnify",
} }
/** /**
@ -163,6 +164,18 @@ export interface LayoutTreeResizeNodeAction extends LayoutTreeAction {
resizeOperations: ResizeNodeOperation[]; resizeOperations: ResizeNodeOperation[];
} }
/**
* Action for toggling magnification of a node from the layout tree.
*/
export interface LayoutTreeMagnifyNodeToggleAction extends LayoutTreeAction {
type: LayoutTreeActionType.MagnifyNodeToggle;
/**
* The id of the node to maximize;
*/
nodeId: string;
}
/** /**
* Represents the state of a layout tree. * Represents the state of a layout tree.
* *
@ -173,6 +186,7 @@ export type LayoutTreeState<T> = {
leafs: LayoutNode<T>[]; leafs: LayoutNode<T>[];
pendingAction: LayoutTreeAction; pendingAction: LayoutTreeAction;
generation: number; generation: number;
magnifiedNodeId?: string;
}; };
/** /**
@ -200,6 +214,7 @@ export type WritableLayoutTreeStateAtom<T> = WritableAtom<LayoutTreeState<T>, [v
export type ContentRenderer<T> = ( export type ContentRenderer<T> = (
data: T, data: T,
ready: boolean, ready: boolean,
onMagnifyToggle: () => void,
onClose: () => void, onClose: () => void,
dragHandleRef: React.RefObject<HTMLDivElement> dragHandleRef: React.RefObject<HTMLDivElement>
) => React.ReactNode; ) => React.ReactNode;
@ -208,6 +223,7 @@ export type PreviewRenderer<T> = (data: T) => React.ReactElement;
export interface LayoutNodeWaveObj<T> extends WaveObj { export interface LayoutNodeWaveObj<T> extends WaveObj {
node: LayoutNode<T>; node: LayoutNode<T>;
magnifiednodeid: string;
} }
export const DefaultNodeSize = 10; export const DefaultNodeSize = 10;

View File

@ -66,10 +66,18 @@
} }
.tile-node { .tile-node {
border-radius: var(--block-border-radius);
overflow: hidden;
&.dragging { &.dragging {
filter: blur(8px); filter: blur(8px);
} }
&.magnified {
background-color: var(--block-bg-solid-color);
z-index: 10;
}
.tile-preview-container { .tile-preview-container {
position: absolute; position: absolute;
top: 10000px; top: 10000px;
@ -89,7 +97,7 @@
.placeholder { .placeholder {
transition-duration: 0.15s; transition-duration: 0.15s;
transition-timing-function: ease-in; transition-timing-function: ease-in;
transition-property: transform, width, height; transition-property: transform, width, height, background-color;
} }
} }
@ -106,6 +114,6 @@
.placeholder { .placeholder {
background-color: var(--accent-color); background-color: var(--accent-color);
opacity: 0.5; opacity: 0.5;
border-radius: 5px; border-radius: calc(var(--block-border-radius) + 2px);
} }
} }

View File

@ -176,6 +176,7 @@ declare global {
// wstore.LayoutNode // wstore.LayoutNode
type LayoutNode = WaveObj & { type LayoutNode = WaveObj & {
node?: any; node?: any;
magnifiednodeid?: string;
meta?: MetaType; meta?: MetaType;
}; };
@ -315,7 +316,7 @@ declare global {
// wstore.Tab // wstore.Tab
type Tab = WaveObj & { type Tab = WaveObj & {
name: string; name: string;
layoutNode: string; layoutnode: string;
blockids: string[]; blockids: string[];
meta: MetaType; meta: MetaType;
}; };

View File

@ -207,7 +207,7 @@ type Tab struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`
Name string `json:"name"` Name string `json:"name"`
LayoutNode string `json:"layoutNode"` LayoutNode string `json:"layoutnode"`
BlockIds []string `json:"blockids"` BlockIds []string `json:"blockids"`
Meta map[string]any `json:"meta"` Meta map[string]any `json:"meta"`
} }
@ -228,6 +228,7 @@ type LayoutNode struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`
Node any `json:"node,omitempty"` Node any `json:"node,omitempty"`
MagnifiedNodeId string `json:"magnifiednodeid,omitempty"`
Meta map[string]any `json:"meta,omitempty"` Meta map[string]any `json:"meta,omitempty"`
} }