Add resize handles to the layout system (#66)

Adds resizability to the layout system.

Hovering in the margins of a block will highlight the available resize
handle and show a cursor indicating its resize direction. Dragging will
cause the resizing nodes to blur out and be replaced by an outline.
Releasing the handle will commit the new resize operation and cause the
underlying nodes to update to their new sizes.

We'll want to refactor this in the future to move all layout and resize
logic into a shared model that the TileLayout code can talk to, but
that's a future improvement. For now, this makes some compromises,
mainly that the logic is kind of distributed around.

---------

Co-authored-by: sawka <mike.sawka@gmail.com>
This commit is contained in:
Evan Simkowitz
2024-07-03 14:31:02 -07:00
committed by GitHub
co-authored by sawka
parent b2cdd441d1
commit 75c9e211d9
15 changed files with 518 additions and 178 deletions
+1 -1
View File
@@ -161,7 +161,7 @@ function switchBlock(tabId: string, offsetX: number, offsetY: number) {
return;
}
const layoutTreeState = globalStore.get(getLayoutStateAtomForTab(tabId, tabAtom));
const curBlockId = globalStore.get(atoms.waveWindow).activeblockid;
const curBlockId = globalStore.get(atoms.waveWindow)?.activeblockid;
const curBlockLeafId = layoututil.findLeafIdFromBlockId(layoutTreeState, curBlockId);
if (curBlockLeafId == null) {
return;
+4 -8
View File
@@ -55,11 +55,11 @@
&.block-frame-tech {
border: 2px solid var(--border-color);
border-radius: 4px;
margin: 10px 2px 2px 2px;
border-radius: 7px;
margin: 8px 0 0 0;
padding: 10px 2px 2px 2px;
height: calc(100% - 12px);
width: calc(100% - 4px);
height: calc(100% - 8px);
width: 100%;
overflow: visible;
&.block-preview {
@@ -87,7 +87,6 @@
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);
@@ -100,10 +99,7 @@
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 {
+1 -1
View File
@@ -224,7 +224,7 @@ const BlockFrame_Tech_Component = ({
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
return jotai.atom((get) => {
const winData = get(atoms.waveWindow);
return winData.activeblockid === blockId;
return winData?.activeblockid === blockId;
});
});
let isFocused = jotai.useAtomValue(isFocusedAtom);
+3
View File
@@ -341,6 +341,9 @@ async function fetchWaveFile(
function setBlockFocus(blockId: string) {
let winData = globalStore.get(atoms.waveWindow);
if (winData == null) {
return;
}
if (winData.activeblockid === blockId) {
return;
}
+1
View File
@@ -11,6 +11,7 @@
justify-content: center;
overflow: hidden;
position: relative;
margin: 3px;
.block-container {
display: flex;
+1 -1
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { Block, BlockFrame } from "@/app/block/block";
import { getApi } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import * as React from "react";
@@ -11,7 +12,6 @@ import { TileLayout } from "@/faraday/index";
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { useAtomValue } from "jotai";
import { useMemo } from "react";
import { getApi } from "../store/global";
import "./tabcontent.less";
const TabContent = React.memo(({ tabId }: { tabId: string }) => {
+10 -5
View File
@@ -142,6 +142,9 @@ const IJSONConst = {
function setBlockFocus(blockId: string) {
let winData = globalStore.get(atoms.waveWindow);
if (winData == null) {
return;
}
winData = produce(winData, (draft) => {
draft.activeblockid = blockId;
});
@@ -160,7 +163,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
return jotai.atom((get) => {
const winData = get(atoms.waveWindow);
return winData.activeblockid === blockId;
return winData?.activeblockid === blockId;
});
});
const termSettingsAtom = useSettingsAtom<TerminalConfigType>("term", (settings: SettingsConfigType) => {
@@ -240,6 +243,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
termMode = "term";
}
// set initial focus
React.useEffect(() => {
if (isFocused && termMode == "term") {
termRef.current?.terminal.focus();
@@ -247,8 +251,9 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
if (isFocused && termMode == "html") {
htmlElemFocusRef.current?.focus();
}
});
}, []);
// set intitial controller status, and then subscribe for updates
React.useEffect(() => {
function updateShellProcStatus(status: string) {
if (status == null) {
@@ -268,12 +273,12 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
updateShellProcStatus(rts?.shellprocstatus);
});
const bcSubject = getEventORefSubject("blockcontroller:status", WOS.makeORef("block", blockId));
bcSubject.subscribe((data: WSEventType) => {
const sub = bcSubject.subscribe((data: WSEventType) => {
let bcRTS: BlockControllerRuntimeStatus = data.data;
updateShellProcStatus(bcRTS?.shellprocstatus);
});
return undefined;
});
return () => sub.unsubscribe();
}, []);
let stickerConfig = {
charWidth: 8,
+21 -22
View File
@@ -10,7 +10,6 @@ import { newLayoutTreeStateAtom, useLayoutTreeStateReducerAtom } from "./layoutA
import { newLayoutNode } from "./layoutNode.js";
import { LayoutTreeActionType, LayoutTreeInsertNodeAction, WritableLayoutTreeStateAtom } from "./model.js";
import "./tilelayout.stories.less";
import { FlexDirection } from "./utils.js";
interface TestData {
name: string;
@@ -22,7 +21,7 @@ const meta = {
title: "TileLayout",
args: {
layoutTreeStateAtom: newLayoutTreeStateAtom<TestData>(
newLayoutNode(FlexDirection.Row, undefined, undefined, {
newLayoutNode(undefined, undefined, undefined, {
name: "Hello world!",
})
),
@@ -52,7 +51,7 @@ type Story = StoryObj<typeof meta>;
export const Basic: Story = {
args: {
layoutTreeStateAtom: newLayoutTreeStateAtom<TestData>(
newLayoutNode(FlexDirection.Row, undefined, undefined, { name: "Hello world!" })
newLayoutNode(undefined, undefined, undefined, { name: "Hello world!" })
),
},
};
@@ -60,31 +59,31 @@ export const Basic: Story = {
export const More: Story = {
args: {
layoutTreeStateAtom: newLayoutTreeStateAtom<TestData>(
newLayoutNode(FlexDirection.Row, undefined, [
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world1!" }),
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world2!" }),
newLayoutNode(FlexDirection.Column, undefined, [
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world3!" }),
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world4!" }),
newLayoutNode(undefined, undefined, [
newLayoutNode(undefined, undefined, undefined, { name: "Hello world1!" }),
newLayoutNode(undefined, undefined, undefined, { name: "Hello world2!" }),
newLayoutNode(undefined, undefined, [
newLayoutNode(undefined, undefined, undefined, { name: "Hello world3!" }),
newLayoutNode(undefined, undefined, undefined, { name: "Hello world4!" }),
]),
])
),
},
};
const evenMoreRootNode = newLayoutNode<TestData>(FlexDirection.Row, undefined, [
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world1!" }),
newLayoutNode(FlexDirection.Column, undefined, [
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world2!" }),
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world3!" }),
const evenMoreRootNode = newLayoutNode<TestData>(undefined, undefined, [
newLayoutNode(undefined, undefined, undefined, { name: "Hello world1!" }),
newLayoutNode(undefined, undefined, [
newLayoutNode(undefined, undefined, undefined, { name: "Hello world2!" }),
newLayoutNode(undefined, undefined, undefined, { name: "Hello world3!" }),
]),
newLayoutNode(FlexDirection.Column, undefined, [
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world4!" }),
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world5!" }),
newLayoutNode(FlexDirection.Column, undefined, [
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world6!" }),
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world7!" }),
newLayoutNode(FlexDirection.Column, undefined, undefined, { name: "Hello world8!" }),
newLayoutNode(undefined, undefined, [
newLayoutNode(undefined, undefined, undefined, { name: "Hello world4!" }),
newLayoutNode(undefined, undefined, undefined, { name: "Hello world5!" }),
newLayoutNode(undefined, undefined, [
newLayoutNode(undefined, undefined, undefined, { name: "Hello world6!" }),
newLayoutNode(undefined, undefined, undefined, { name: "Hello world7!" }),
newLayoutNode(undefined, undefined, undefined, { name: "Hello world8!" }),
]),
]),
]);
@@ -102,7 +101,7 @@ export const AddNode: Story = {
const [, dispatch] = useLayoutTreeStateReducerAtom(addNodeAtom);
const [numAddedNodes, setNumAddedNodes] = useState(0);
const dispatchAddNode = () => {
const newNode = newLayoutNode(FlexDirection.Column, undefined, undefined, {
const newNode = newLayoutNode(undefined, undefined, undefined, {
name: "New Node" + numAddedNodes,
});
const insertNodeAction: LayoutTreeInsertNodeAction<TestData> = {
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,7 +1,7 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { LayoutNode } from "./model";
import { DefaultNodeSize, LayoutNode } from "./model";
import { FlexDirection, getCrypto, reverseFlexDirection } from "./utils";
const crypto = getCrypto();
@@ -24,7 +24,7 @@ export function newLayoutNode<T>(
const newNode: LayoutNode<T> = {
id: crypto.randomUUID(),
flexDirection: flexDirection ?? FlexDirection.Row,
size,
size: size ?? DefaultNodeSize,
children,
data,
};
@@ -182,14 +182,14 @@ function balanceNodeHelper<T>(node: LayoutNode<T>, leafs: LayoutNode<T>[]): Layo
leafs.push(node);
return node;
}
if (node.children.length === 0) return;
if (node.children.length == 0) return;
if (!validateNode(node)) throw new Error("Invalid node");
node.children = node.children
.flatMap((child) => {
if (child.flexDirection === node.flexDirection) {
child.flexDirection = reverseFlexDirection(node.flexDirection);
}
if (child.children?.length === 1 && child.children[0].children) {
if (child.children?.length == 1 && child.children[0].children) {
return child.children[0].children;
}
return child;
@@ -198,7 +198,7 @@ function balanceNodeHelper<T>(node: LayoutNode<T>, leafs: LayoutNode<T>[]): Layo
return balanceNodeHelper(child, leafs);
})
.filter((v) => v);
if (node.children.length === 1 && !node.children[0].children) {
if (node.children.length == 1 && !node.children[0].children) {
node.data = node.children[0].data;
node.id = node.children[0].id;
node.children = undefined;
+65 -21
View File
@@ -11,6 +11,7 @@ import {
removeChild,
} from "./layoutNode";
import {
DefaultNodeSize,
LayoutNode,
LayoutTreeAction,
LayoutTreeActionType,
@@ -18,6 +19,8 @@ import {
LayoutTreeDeleteNodeAction,
LayoutTreeInsertNodeAction,
LayoutTreeMoveNodeAction,
LayoutTreeResizeNodeAction,
LayoutTreeSetPendingAction,
LayoutTreeState,
LayoutTreeSwapNodeAction,
MoveOperation,
@@ -70,8 +73,11 @@ function layoutTreeStateReducerInner<T>(layoutTreeState: LayoutTreeState<T>, act
case LayoutTreeActionType.ComputeMove:
computeMoveNode(layoutTreeState, action as LayoutTreeComputeMoveNodeAction<T>);
break;
case LayoutTreeActionType.SetPendingAction:
setPendingAction(layoutTreeState, action as LayoutTreeSetPendingAction);
break;
case LayoutTreeActionType.ClearPendingAction:
clearPendingAction(layoutTreeState);
layoutTreeState.pendingAction = undefined;
break;
case LayoutTreeActionType.CommitPendingAction:
if (!layoutTreeState?.pendingAction) {
@@ -79,6 +85,7 @@ function layoutTreeStateReducerInner<T>(layoutTreeState: LayoutTreeState<T>, act
break;
}
layoutTreeStateReducerInner(layoutTreeState, layoutTreeState.pendingAction);
layoutTreeState.pendingAction = undefined;
break;
case LayoutTreeActionType.Move:
moveNode(layoutTreeState, action as LayoutTreeMoveNodeAction<T>);
@@ -93,7 +100,11 @@ function layoutTreeStateReducerInner<T>(layoutTreeState: LayoutTreeState<T>, act
layoutTreeState.generation++;
break;
case LayoutTreeActionType.Swap:
swapNode(layoutTreeState, action as LayoutTreeSwapNodeAction<T>);
swapNode(layoutTreeState, action as LayoutTreeSwapNodeAction);
layoutTreeState.generation++;
break;
case LayoutTreeActionType.ResizeNode:
resizeNode(layoutTreeState, action as LayoutTreeResizeNodeAction);
layoutTreeState.generation++;
break;
default: {
@@ -260,10 +271,10 @@ function computeMoveNode<T>(
case DropDirection.Center:
// console.log("center drop", rootNode, node, nodeToMove);
if (node.id !== rootNode.id && nodeToMove.id !== rootNode.id) {
const swapAction: LayoutTreeSwapNodeAction<T> = {
const swapAction: LayoutTreeSwapNodeAction = {
type: LayoutTreeActionType.Swap,
node1: node,
node2: nodeToMove,
node1Id: node.id,
node2Id: nodeToMove.id,
};
// console.log("swapAction", swapAction);
layoutTreeState.pendingAction = swapAction;
@@ -287,8 +298,12 @@ function computeMoveNode<T>(
} as LayoutTreeMoveNodeAction<T>;
}
function clearPendingAction(layoutTreeState: LayoutTreeState<any>) {
layoutTreeState.pendingAction = undefined;
function setPendingAction(layoutTreeState: LayoutTreeState<any>, action: LayoutTreeSetPendingAction) {
if (action.action === undefined) {
console.error("setPendingAction: invalid pending action passed to function");
return;
}
layoutTreeState.pendingAction = action.action;
}
function moveNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMoveNodeAction<T>) {
@@ -313,10 +328,15 @@ function moveNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMove
// If moving under the same parent, we need to make sure that we are removing the child from its old position, not its new one.
// If the new index is before the old index, we need to start our search for the node to delete after the new index position.
if (oldParent && parent && oldParent.id === parent.id) {
const curIndexInParent = parent.children!.indexOf(node);
if (curIndexInParent >= action.index) {
startingIndex = action.index + 1;
// If a node is being moved under the same parent, it can keep its size. Otherwise, it should get reset.
if (oldParent && parent) {
if (oldParent.id === parent.id) {
const curIndexInParent = parent.children!.indexOf(node);
if (curIndexInParent >= action.index) {
startingIndex = action.index + 1;
}
} else {
node.size = DefaultNodeSize;
}
}
@@ -360,28 +380,37 @@ function insertNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeIn
layoutTreeState.leafs = leafs;
}
function swapNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeSwapNodeAction<T>) {
function swapNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeSwapNodeAction) {
console.log("swapNode", layoutTreeState, action);
if (!action.node1 || !action.node2) {
if (!action.node1Id || !action.node2Id) {
console.error("invalid swapNode action, both node1 and node2 must be defined");
return;
}
if (action.node1.id === layoutTreeState.rootNode.id || action.node2.id === layoutTreeState.rootNode.id) {
if (action.node1Id === layoutTreeState.rootNode.id || action.node2Id === layoutTreeState.rootNode.id) {
console.error("invalid swapNode action, the root node cannot be swapped");
return;
}
if (action.node1.id === action.node2.id) {
if (action.node1Id === action.node2Id) {
console.error("invalid swapNode action, node1 and node2 are equal");
return;
}
const parentNode1 = findParent(layoutTreeState.rootNode, action.node1.id);
const parentNode2 = findParent(layoutTreeState.rootNode, action.node2.id);
const parentNode1Index = parentNode1.children!.findIndex((child) => child.id === action.node1.id);
const parentNode2Index = parentNode2.children!.findIndex((child) => child.id === action.node2.id);
const parentNode1 = findParent(layoutTreeState.rootNode, action.node1Id);
const parentNode2 = findParent(layoutTreeState.rootNode, action.node2Id);
const parentNode1Index = parentNode1.children!.findIndex((child) => child.id === action.node1Id);
const parentNode2Index = parentNode2.children!.findIndex((child) => child.id === action.node2Id);
parentNode1.children[parentNode1Index] = action.node2;
parentNode2.children[parentNode2Index] = action.node1;
const node1 = parentNode1.children![parentNode1Index];
const node2 = parentNode2.children![parentNode2Index];
const node1Size = node1.size;
node1.size = node2.size;
node2.size = node1Size;
parentNode1.children[parentNode1Index] = node2;
parentNode2.children[parentNode2Index] = node1;
const { node: newRootNode, leafs } = balanceNode(layoutTreeState.rootNode);
layoutTreeState.rootNode = newRootNode;
@@ -415,3 +444,18 @@ function deleteNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeDe
layoutTreeState.rootNode = newRootNode;
layoutTreeState.leafs = leafs;
}
function resizeNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeResizeNodeAction) {
console.log("resizeNode", layoutTreeState, action);
if (!action.resizeOperations) {
console.error("invalid resizeNode operation. nodeSizes array must be defined.");
}
for (const resize of action.resizeOperations) {
if (!resize.nodeId || resize.size < 0 || resize.size > 100) {
console.error("invalid resizeNode operation. nodeId must be defined and size must be between 0 and 100");
return;
}
const node = findNode(layoutTreeState.rootNode, resize.nodeId);
node.size = resize.size;
}
}
+55 -14
View File
@@ -33,11 +33,12 @@ export type MoveOperation<T> = {
* Types of actions that modify the layout tree.
*/
export enum LayoutTreeActionType {
ComputeMove = "compute",
ComputeMove = "computemove",
Move = "move",
Swap = "swap",
CommitPendingAction = "commit",
ClearPendingAction = "clear",
SetPendingAction = "setpending",
CommitPendingAction = "commitpending",
ClearPendingAction = "clearpending",
ResizeNode = "resize",
InsertNode = "insert",
DeleteNode = "delete",
@@ -79,24 +80,17 @@ export interface LayoutTreeMoveNodeAction<T> extends LayoutTreeAction, MoveOpera
*
* @template T The type of data associated with the nodes of the tree.
*/
export interface LayoutTreeSwapNodeAction<T> extends LayoutTreeAction {
export interface LayoutTreeSwapNodeAction extends LayoutTreeAction {
type: LayoutTreeActionType.Swap;
/**
* The node that node2 will replace.
*/
node1: LayoutNode<T>;
node1Id: string;
/**
* The node that node1 will replace.
*/
node2: LayoutNode<T>;
}
/**
* Action for committing a pending action to the layout tree.
*/
export interface LayoutTreeCommitPendingAction extends LayoutTreeAction {
type: LayoutTreeActionType.CommitPendingAction;
node2Id: string;
}
/**
@@ -117,6 +111,25 @@ export interface LayoutTreeDeleteNodeAction extends LayoutTreeAction {
nodeId: string;
}
/**
* Action for setting the pendingAction field of the layout tree state.
*/
export interface LayoutTreeSetPendingAction extends LayoutTreeAction {
type: LayoutTreeActionType.SetPendingAction;
/**
* The new value for the pending action field.
*/
action: LayoutTreeAction;
}
/**
* Action for committing the action in the pendingAction field of the layout tree state.
*/
export interface LayoutTreeCommitPendingAction extends LayoutTreeAction {
type: LayoutTreeActionType.CommitPendingAction;
}
/**
* Action for clearing the pendingAction field from the layout tree state.
*/
@@ -124,6 +137,32 @@ export interface LayoutTreeClearPendingAction extends LayoutTreeAction {
type: LayoutTreeActionType.ClearPendingAction;
}
/**
* An operation to resize a node.
*/
export interface ResizeNodeOperation {
/**
* The id of the node to resize.
*/
nodeId: string;
/**
* The new size for the node.
*/
size: number;
}
/**
* Action for resizing a node from the layout tree.
*/
export interface LayoutTreeResizeNodeAction extends LayoutTreeAction {
type: LayoutTreeActionType.ResizeNode;
/**
* A list of node ids to update and their respective new sizes.
*/
resizeOperations: ResizeNodeOperation[];
}
/**
* Represents the state of a layout tree.
*
@@ -145,7 +184,7 @@ export interface LayoutNode<T> {
data?: T;
children?: LayoutNode<T>[];
flexDirection: FlexDirection;
size?: number;
size: number;
}
/**
@@ -170,3 +209,5 @@ export type PreviewRenderer<T> = (data: T) => React.ReactElement;
export interface LayoutNodeWaveObj<T> extends WaveObj {
node: LayoutNode<T>;
}
export const DefaultNodeSize = 10;
+22
View File
@@ -0,0 +1,22 @@
export class NodeRefMap {
private map: Map<string, React.RefObject<HTMLDivElement>> = new Map();
generation: number = 0;
set(id: string, ref: React.RefObject<HTMLDivElement>) {
this.map.set(id, ref);
this.generation++;
}
delete(id: string) {
if (this.map.has(id)) {
this.map.delete(id);
this.generation++;
}
}
get(id: string): React.RefObject<HTMLDivElement> {
if (this.map.has(id)) {
return this.map.get(id);
}
}
}
+36 -13
View File
@@ -16,6 +16,8 @@
left: 0;
height: 100%;
width: 100%;
min-height: 4rem;
min-width: 4rem;
}
.placeholder-container {
@@ -29,14 +31,38 @@
.tile-node,
.overlay-node {
display: flex;
flex: 1 1 auto;
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
flex: 0 1 auto;
}
&.is-dragging {
background-color: rgba(0, 255, 0, 0.3);
.overlay-node {
.resize-handle {
&.flex-row {
cursor: ew-resize;
.line {
height: 100%;
margin-left: 2px;
}
}
&.flex-column {
cursor: ns-resize;
.line {
margin-top: 3px;
}
}
flex: 0 0 5px;
&:hover {
&.flex-row .line {
border-left: 1px solid var(--accent-color);
}
&.flex-column .line {
border-bottom: 1px solid var(--accent-color);
}
}
}
&.resizing {
border: 1px solid var(--accent-color);
backdrop-filter: blur(8px);
}
}
@@ -70,19 +96,16 @@
.tile-leaf,
.overlay-leaf {
display: flex;
flex: 1 1 auto;
max-height: 100%;
max-width: 100%;
height: 100%;
width: 100%;
}
.tile-leaf {
border: 1px solid black;
overflow: hidden;
}
.placeholder {
background-color: aqua;
background-color: var(--accent-color);
opacity: 0.5;
border-radius: 5px;
}
+2
View File
@@ -1,6 +1,8 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type * as rxjs from "rxjs";
declare global {
type TabLayoutData = {
blockId: string;