mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-21 15:58:30 +00:00
update widgets bar to be MUCH more responsive (#2311)
This commit is contained in:
parent
0f7159858e
commit
76afdc65ce
5 changed files with 294 additions and 222 deletions
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { cn } from "@/util/util";
|
||||
import {
|
||||
FloatingPortal,
|
||||
autoUpdate,
|
||||
|
|
@ -11,7 +12,6 @@ import {
|
|||
useHover,
|
||||
useInteractions,
|
||||
} from "@floating-ui/react";
|
||||
import { cn } from "@/util/util";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface TooltipProps {
|
||||
|
|
@ -19,12 +19,13 @@ interface TooltipProps {
|
|||
content: React.ReactNode;
|
||||
placement?: "top" | "bottom" | "left" | "right";
|
||||
forceOpen?: boolean;
|
||||
disable?: boolean;
|
||||
divClassName?: string;
|
||||
divStyle?: React.CSSProperties;
|
||||
divOnClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
export function Tooltip({
|
||||
function TooltipInner({
|
||||
children,
|
||||
content,
|
||||
placement = "top",
|
||||
|
|
@ -32,7 +33,7 @@ export function Tooltip({
|
|||
divClassName,
|
||||
divStyle,
|
||||
divOnClick,
|
||||
}: TooltipProps) {
|
||||
}: Omit<TooltipProps, 'disable'>) {
|
||||
const [isOpen, setIsOpen] = useState(forceOpen);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
|
@ -63,11 +64,7 @@ export function Tooltip({
|
|||
}
|
||||
},
|
||||
placement,
|
||||
middleware: [
|
||||
offset(10),
|
||||
flip(),
|
||||
shift({ padding: 12 }),
|
||||
],
|
||||
middleware: [offset(10), flip(), shift({ padding: 12 })],
|
||||
whileElementsMounted: autoUpdate,
|
||||
});
|
||||
|
||||
|
|
@ -132,7 +129,7 @@ export function Tooltip({
|
|||
}}
|
||||
{...getFloatingProps()}
|
||||
className={cn(
|
||||
"bg-white dark:bg-panel border border-border rounded-md px-2 py-1 text-xs text-foreground shadow-xl z-50"
|
||||
"bg-gray-800 border border-border rounded-md px-2 py-1 text-xs text-foreground shadow-xl z-50"
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
|
|
@ -141,4 +138,39 @@ export function Tooltip({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function Tooltip({
|
||||
children,
|
||||
content,
|
||||
placement = "top",
|
||||
forceOpen = false,
|
||||
disable = false,
|
||||
divClassName,
|
||||
divStyle,
|
||||
divOnClick,
|
||||
}: TooltipProps) {
|
||||
if (disable) {
|
||||
return (
|
||||
<div
|
||||
className={divClassName}
|
||||
style={divStyle}
|
||||
onClick={divOnClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipInner
|
||||
children={children}
|
||||
content={content}
|
||||
placement={placement}
|
||||
forceOpen={forceOpen}
|
||||
divClassName={divClassName}
|
||||
divStyle={divStyle}
|
||||
divOnClick={divOnClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.notification-popover {
|
||||
padding: 8px 2px 8px 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
width: 27px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
i {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
display: flex;
|
||||
width: 380px;
|
||||
padding: 10px 0 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
column-gap: 8px;
|
||||
border-radius: 8px;
|
||||
border: 0.5px solid rgba(255, 255, 255, 0.12);
|
||||
background: #232323;
|
||||
box-shadow: 0px 8px 32px 0px rgba(0, 0, 0, 0.25);
|
||||
|
||||
.divider {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 10px 8px 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
|
||||
span {
|
||||
color: var(--main-text-color);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.close-all-btn {
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,8 +13,6 @@ import { NotificationItem } from "./notificationitem";
|
|||
import { useUpdateNotifier } from "./updatenotifier";
|
||||
import { useNotification } from "./usenotification";
|
||||
|
||||
import "./notificationpopover.scss";
|
||||
|
||||
const NotificationPopover = () => {
|
||||
useUpdateNotifier();
|
||||
const {
|
||||
|
|
@ -51,14 +49,14 @@ const NotificationPopover = () => {
|
|||
|
||||
return (
|
||||
<Popover
|
||||
className="notification-popover"
|
||||
className="w-full pb-2 pt-1 pl-0 pr-0.5 flex items-center justify-center"
|
||||
placement="left-end"
|
||||
offset={{ mainAxis: 20, crossAxis: 2 }}
|
||||
onDismiss={handleTogglePopover}
|
||||
>
|
||||
<PopoverButton
|
||||
className={clsx(
|
||||
"notification-trigger-button horizontal-padding-6 vertical-padding-4 border-radius-",
|
||||
"w-[27px] h-[26px] flex justify-center [&>i]:text-[17px] horizontal-padding-6 vertical-padding-4 border-radius-",
|
||||
addOnClassNames
|
||||
)}
|
||||
disabled={notifications.length === 0}
|
||||
|
|
@ -67,11 +65,11 @@ const NotificationPopover = () => {
|
|||
{getIcon()}
|
||||
</PopoverButton>
|
||||
{notifications.length > 0 && (
|
||||
<PopoverContent className="notification-content">
|
||||
<div className="header">
|
||||
<span>Notifications</span>
|
||||
<PopoverContent className="flex w-[380px] pt-2.5 pb-0 px-0 flex-col items-start gap-x-2 rounded-lg border-[0.5px] border-white/12 bg-[#232323] shadow-[0px_8px_32px_0px_rgba(0,0,0,0.25)]">
|
||||
<div className="flex items-center justify-between w-full px-2.5 pb-2 border-b border-white/8">
|
||||
<span className="text-foreground text-sm font-semibold leading-4">Notifications</span>
|
||||
<Button
|
||||
className="ghost grey close-all-btn horizontal-padding-3 vertical-padding-3"
|
||||
className="ghost grey text-[13px] font-normal leading-4 text-white/40 horizontal-padding-3 vertical-padding-3"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeAllNotifications();
|
||||
|
|
@ -98,7 +96,7 @@ const NotificationPopover = () => {
|
|||
onMouseEnter={() => setHoveredId(notif.id)}
|
||||
onMouseLeave={() => setHoveredId(null)}
|
||||
/>
|
||||
{index !== notifications.length - 1 && <div className="divider"></div>}
|
||||
{index !== notifications.length - 1 && <div className="bg-white/8 h-px w-full"></div>}
|
||||
</Fragment>
|
||||
))}
|
||||
</OverlayScrollbarsComponent>
|
||||
|
|
|
|||
243
frontend/app/workspace/widgets.tsx
Normal file
243
frontend/app/workspace/widgets.tsx
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Tooltip } from "@/app/element/tooltip";
|
||||
import { NotificationPopover } from "@/app/notification/notificationpopover";
|
||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { atoms, createBlock, getApi, isDev } from "@/store/global";
|
||||
import { fireAndForget, isBlank, makeIconClass } from "@/util/util";
|
||||
import clsx from "clsx";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
function sortByDisplayOrder(wmap: { [key: string]: WidgetConfigType }): WidgetConfigType[] {
|
||||
if (wmap == null) {
|
||||
return [];
|
||||
}
|
||||
const wlist = Object.values(wmap);
|
||||
wlist.sort((a, b) => {
|
||||
return (a["display:order"] ?? 0) - (b["display:order"] ?? 0);
|
||||
});
|
||||
return wlist;
|
||||
}
|
||||
|
||||
async function handleWidgetSelect(widget: WidgetConfigType) {
|
||||
const blockDef = widget.blockdef;
|
||||
createBlock(blockDef, widget.magnified);
|
||||
}
|
||||
|
||||
const Widget = memo(({ widget, mode }: { widget: WidgetConfigType; mode: "normal" | "compact" | "supercompact" }) => {
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
const labelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "normal" && labelRef.current) {
|
||||
const element = labelRef.current;
|
||||
setIsTruncated(element.scrollWidth > element.clientWidth);
|
||||
}
|
||||
}, [mode, widget.label]);
|
||||
|
||||
const shouldDisableTooltip = mode !== "normal" ? false : !isTruncated;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={widget.description || widget.label}
|
||||
placement="left"
|
||||
disable={shouldDisableTooltip}
|
||||
divClassName={clsx(
|
||||
"flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-secondary overflow-hidden rounded-sm hover:bg-hoverbg hover:text-white cursor-pointer",
|
||||
mode === "supercompact" ? "text-sm" : "text-lg",
|
||||
widget["display:hidden"] && "hidden"
|
||||
)}
|
||||
divOnClick={() => handleWidgetSelect(widget)}
|
||||
>
|
||||
<div style={{ color: widget.color }}>
|
||||
<i className={makeIconClass(widget.icon, true, { defaultIcon: "browser" })}></i>
|
||||
</div>
|
||||
{mode === "normal" && !isBlank(widget.label) ? (
|
||||
<div
|
||||
ref={labelRef}
|
||||
className="text-xxs mt-0.5 w-full px-0.5 text-center whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
>
|
||||
{widget.label}
|
||||
</div>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
const Widgets = memo(() => {
|
||||
const fullConfig = useAtomValue(atoms.fullConfigAtom);
|
||||
const [mode, setMode] = useState<"normal" | "compact" | "supercompact">("normal");
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const measurementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const helpWidget: WidgetConfigType = {
|
||||
icon: "circle-question",
|
||||
label: "help",
|
||||
blockdef: {
|
||||
meta: {
|
||||
view: "help",
|
||||
},
|
||||
},
|
||||
};
|
||||
const tipsWidget: WidgetConfigType = {
|
||||
icon: "lightbulb",
|
||||
label: "tips",
|
||||
blockdef: {
|
||||
meta: {
|
||||
view: "tips",
|
||||
},
|
||||
},
|
||||
};
|
||||
const showHelp = fullConfig?.settings?.["widget:showhelp"] ?? true;
|
||||
const widgets = sortByDisplayOrder(fullConfig?.widgets);
|
||||
|
||||
const checkModeNeeded = useCallback(() => {
|
||||
if (!containerRef.current || !measurementRef.current) return;
|
||||
|
||||
const containerHeight = containerRef.current.clientHeight;
|
||||
const normalHeight = measurementRef.current.scrollHeight;
|
||||
const gracePeriod = 10;
|
||||
|
||||
let newMode: "normal" | "compact" | "supercompact" = "normal";
|
||||
|
||||
if (normalHeight > containerHeight - gracePeriod) {
|
||||
newMode = "compact";
|
||||
|
||||
// Calculate total widget count for supercompact check
|
||||
const totalWidgets = (widgets?.length || 0) + (showHelp ? 2 : 0);
|
||||
const minHeightPerWidget = 32;
|
||||
const requiredHeight = totalWidgets * minHeightPerWidget;
|
||||
|
||||
if (requiredHeight > containerHeight) {
|
||||
newMode = "supercompact";
|
||||
}
|
||||
}
|
||||
|
||||
if (newMode !== mode) {
|
||||
setMode(newMode);
|
||||
}
|
||||
}, [mode, widgets, showHelp]);
|
||||
|
||||
useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
checkModeNeeded();
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [checkModeNeeded]);
|
||||
|
||||
useEffect(() => {
|
||||
checkModeNeeded();
|
||||
}, [widgets, showHelp, checkModeNeeded]);
|
||||
|
||||
const handleWidgetsBarContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const menu: ContextMenuItem[] = [
|
||||
{
|
||||
label: "Edit widgets.json",
|
||||
click: () => {
|
||||
fireAndForget(async () => {
|
||||
const path = `${getApi().getConfigDir()}/widgets.json`;
|
||||
const blockDef: BlockDef = {
|
||||
meta: { view: "preview", file: path },
|
||||
};
|
||||
await createBlock(blockDef, false, true);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Show Help Widgets",
|
||||
submenu: [
|
||||
{
|
||||
label: "On",
|
||||
type: "checkbox",
|
||||
checked: showHelp,
|
||||
click: () => {
|
||||
fireAndForget(async () => {
|
||||
await RpcApi.SetConfigCommand(TabRpcClient, { "widget:showhelp": true });
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Off",
|
||||
type: "checkbox",
|
||||
checked: !showHelp,
|
||||
click: () => {
|
||||
fireAndForget(async () => {
|
||||
await RpcApi.SetConfigCommand(TabRpcClient, { "widget:showhelp": false });
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
ContextMenuModel.showContextMenu(menu, e);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex flex-col w-12 overflow-hidden py-1 -ml-1 select-none"
|
||||
onContextMenu={handleWidgetsBarContextMenu}
|
||||
>
|
||||
{mode === "supercompact" ? (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-0 w-full">
|
||||
{widgets?.map((data, idx) => <Widget key={`widget-${idx}`} widget={data} mode={mode} />)}
|
||||
</div>
|
||||
<div className="flex-grow" />
|
||||
{showHelp ? (
|
||||
<div className="grid grid-cols-2 gap-0 w-full">
|
||||
<Widget key="tips" widget={tipsWidget} mode={mode} />
|
||||
<Widget key="help" widget={helpWidget} mode={mode} />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{widgets?.map((data, idx) => <Widget key={`widget-${idx}`} widget={data} mode={mode} />)}
|
||||
<div className="flex-grow" />
|
||||
{showHelp ? (
|
||||
<>
|
||||
<Widget key="tips" widget={tipsWidget} mode={mode} />
|
||||
<Widget key="help" widget={helpWidget} mode={mode} />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{isDev() ? <NotificationPopover /> : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={measurementRef}
|
||||
className="flex flex-col w-12 py-1 -ml-1 select-none absolute -z-10 opacity-0 pointer-events-none"
|
||||
>
|
||||
{widgets?.map((data, idx) => (
|
||||
<Widget key={`measurement-widget-${idx}`} widget={data} mode="normal" />
|
||||
))}
|
||||
<div className="flex-grow" />
|
||||
{showHelp ? (
|
||||
<>
|
||||
<Widget key="measurement-tips" widget={tipsWidget} mode="normal" />
|
||||
<Widget key="measurement-help" widget={helpWidget} mode="normal" />
|
||||
</>
|
||||
) : null}
|
||||
{isDev() ? <NotificationPopover /> : null}
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export { Widgets };
|
||||
|
|
@ -4,150 +4,13 @@
|
|||
import { ErrorBoundary } from "@/app/element/errorboundary";
|
||||
import { CenteredDiv } from "@/app/element/quickelems";
|
||||
import { ModalsRenderer } from "@/app/modals/modalsrenderer";
|
||||
import { NotificationPopover } from "@/app/notification/notificationpopover";
|
||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { TabBar } from "@/app/tab/tabbar";
|
||||
import { TabContent } from "@/app/tab/tabcontent";
|
||||
import { atoms, createBlock, getApi, isDev } from "@/store/global";
|
||||
import { fireAndForget, isBlank, makeIconClass } from "@/util/util";
|
||||
import clsx from "clsx";
|
||||
import { Widgets } from "@/app/workspace/widgets";
|
||||
import { atoms } from "@/store/global";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo } from "react";
|
||||
|
||||
const iconRegex = /^[a-z0-9-]+$/;
|
||||
|
||||
function keyLen(obj: Object): number {
|
||||
if (obj == null) {
|
||||
return 0;
|
||||
}
|
||||
return Object.keys(obj).length;
|
||||
}
|
||||
|
||||
function sortByDisplayOrder(wmap: { [key: string]: WidgetConfigType }): WidgetConfigType[] {
|
||||
if (wmap == null) {
|
||||
return [];
|
||||
}
|
||||
const wlist = Object.values(wmap);
|
||||
wlist.sort((a, b) => {
|
||||
return (a["display:order"] ?? 0) - (b["display:order"] ?? 0);
|
||||
});
|
||||
return wlist;
|
||||
}
|
||||
|
||||
const Widgets = memo(() => {
|
||||
const fullConfig = useAtomValue(atoms.fullConfigAtom);
|
||||
const helpWidget: WidgetConfigType = {
|
||||
icon: "circle-question",
|
||||
label: "help",
|
||||
blockdef: {
|
||||
meta: {
|
||||
view: "help",
|
||||
},
|
||||
},
|
||||
};
|
||||
const tipsWidget: WidgetConfigType = {
|
||||
icon: "lightbulb",
|
||||
label: "tips",
|
||||
blockdef: {
|
||||
meta: {
|
||||
view: "tips",
|
||||
},
|
||||
},
|
||||
};
|
||||
const showHelp = fullConfig?.settings?.["widget:showhelp"] ?? true;
|
||||
const widgets = sortByDisplayOrder(fullConfig?.widgets);
|
||||
|
||||
const handleWidgetsBarContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const menu: ContextMenuItem[] = [
|
||||
{
|
||||
label: "Edit widgets.json",
|
||||
click: () => {
|
||||
fireAndForget(async () => {
|
||||
const path = `${getApi().getConfigDir()}/widgets.json`;
|
||||
const blockDef: BlockDef = {
|
||||
meta: { view: "preview", file: path },
|
||||
};
|
||||
await createBlock(blockDef, false, true);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Show Help Widgets",
|
||||
submenu: [
|
||||
{
|
||||
label: "On",
|
||||
type: "checkbox",
|
||||
checked: showHelp,
|
||||
click: () => {
|
||||
fireAndForget(async () => {
|
||||
await RpcApi.SetConfigCommand(TabRpcClient, { "widget:showhelp": true });
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Off",
|
||||
type: "checkbox",
|
||||
checked: !showHelp,
|
||||
click: () => {
|
||||
fireAndForget(async () => {
|
||||
await RpcApi.SetConfigCommand(TabRpcClient, { "widget:showhelp": false });
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
ContextMenuModel.showContextMenu(menu, e);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-12 overflow-hidden py-1 -ml-1 select-none"
|
||||
onContextMenu={handleWidgetsBarContextMenu}
|
||||
>
|
||||
{widgets?.map((data, idx) => <Widget key={`widget-${idx}`} widget={data} />)}
|
||||
<div className="flex-grow" />
|
||||
{showHelp ? (
|
||||
<>
|
||||
<Widget key="tips" widget={tipsWidget} />
|
||||
<Widget key="help" widget={helpWidget} />
|
||||
</>
|
||||
) : null}
|
||||
{isDev() ? <NotificationPopover /> : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
async function handleWidgetSelect(widget: WidgetConfigType) {
|
||||
const blockDef = widget.blockdef;
|
||||
createBlock(blockDef, widget.magnified);
|
||||
}
|
||||
|
||||
const Widget = memo(({ widget }: { widget: WidgetConfigType }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-secondary text-lg overflow-hidden rounded-sm hover:bg-hoverbg hover:text-white cursor-pointer",
|
||||
widget["display:hidden"] && "hidden"
|
||||
)}
|
||||
onClick={() => handleWidgetSelect(widget)}
|
||||
title={widget.description || widget.label}
|
||||
>
|
||||
<div style={{ color: widget.color }}>
|
||||
<i className={makeIconClass(widget.icon, true, { defaultIcon: "browser" })}></i>
|
||||
</div>
|
||||
{!isBlank(widget.label) ? (
|
||||
<div className="text-xxs mt-0.5 w-full px-0.5 text-center whitespace-nowrap overflow-hidden">
|
||||
{widget.label}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const WorkspaceElem = memo(() => {
|
||||
const tabId = useAtomValue(atoms.staticTabId);
|
||||
const ws = useAtomValue(atoms.workspace);
|
||||
|
|
|
|||
Loading…
Reference in a new issue