Move icons out of common, clean up visibility story (#251)

This commit is contained in:
Evan Simkowitz 2024-01-25 16:17:03 -08:00 committed by GitHub
parent 34ec4ff39f
commit b97423268c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 203 additions and 203 deletions

View File

@ -1151,42 +1151,3 @@
} }
} }
} }
.front-icon {
margin-right: 5px;
.svg-icon svg {
width: 14px;
height: 14px;
}
font-size: 16px;
}
.positional-icon-inner {
& > div,i {
text-align: center;
align-items: center;
vertical-align: middle;
width: 20px;
margin: auto auto;
}
}
.actions {
.icon {
font-size: 15px;
padding-top: 2.5px;
margin-bottom: -2.5px;
}
}
.status-indicator {
&.error {
color: @term-red;
}
&.success {
color: @term-green;
}
&.output {
color: @term-white;
}
}

View File

@ -1241,81 +1241,6 @@ class Modal extends React.Component<ModalProps> {
} }
} }
interface PositionalIconProps {
children?: React.ReactNode;
}
class FrontIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div className="front-icon positional-icon">
<div className="positional-icon-inner">
{this.props.children}
</div>
</div>
);
}
}
class EndIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div className="end-icon positional-icon">
<div className="positional-icon-inner">
{this.props.children}
</div>
</div>
);
}
}
interface ActionsIconProps {
onClick: React.MouseEventHandler<HTMLDivElement>;
}
class ActionsIcon extends React.Component<ActionsIconProps> {
render() {
return (
<div onClick={this.props.onClick} title="Actions" className="actions">
<div className="icon hoverEffect fa-sharp fa-solid fa-1x fa-ellipsis-vertical"></div>
</div>
);
}
}
interface StatusIndicatorProps {
level: StatusIndicatorLevel;
className?: string;
}
class StatusIndicator extends React.Component<StatusIndicatorProps> {
render() {
const statusIndicatorLevel = this.props.level;
let statusIndicator = null;
if (statusIndicatorLevel != StatusIndicatorLevel.None) {
let statusIndicatorClass = null;
switch (statusIndicatorLevel) {
case StatusIndicatorLevel.Output:
statusIndicatorClass = "output";
break;
case StatusIndicatorLevel.Success:
statusIndicatorClass = "success";
break;
case StatusIndicatorLevel.Error:
statusIndicatorClass = "error";
break;
}
statusIndicator = (
<div
className={`${this.props.className} fa-sharp fa-solid fa-circle-small status-indicator ${statusIndicatorClass}`}
></div>
);
}
return statusIndicator;
}
}
function ShowWaveShellInstallPrompt(callbackFn: () => void) { function ShowWaveShellInstallPrompt(callbackFn: () => void) {
let message: string = ` let message: string = `
In order to use Wave's advanced features like unified history and persistent sessions, Wave installs a small, open-source helper program called WaveShell on your remote machine. WaveShell does not open any external ports and only communicates with your *local* Wave terminal instance over ssh. For more information please see [the docs](https://docs.waveterm.dev/reference/waveshell). In order to use Wave's advanced features like unified history and persistent sessions, Wave installs a small, open-source helper program called WaveShell on your remote machine. WaveShell does not open any external ports and only communicates with your *local* Wave terminal instance over ssh. For more information please see [the docs](https://docs.waveterm.dev/reference/waveshell).
@ -1358,9 +1283,5 @@ export {
LinkButton, LinkButton,
Status, Status,
Modal, Modal,
FrontIcon,
EndIcon,
ActionsIcon,
StatusIndicator,
ShowWaveShellInstallPrompt, ShowWaveShellInstallPrompt,
}; };

View File

@ -0,0 +1,58 @@
@import "../../../app/common/themes/themes.less";
.front-icon {
margin-right: 5px;
}
.positional-icon-visible {
display: inline-block;
}
.positional-icon-hidden {
display: none;
}
.positional-icon {
display: none;
width: 20px;
height: 20px;
.svg-icon svg {
width: 14px;
height: 14px;
}
font-size: 16px;
}
.positional-icon-inner {
display: flex;
width: inherit;
height: 100%;
line-height: inherit;
align-items: center;
justify-content: center;
& > div,i,.svg-icon,span {
width: 20px;
display: flex;
text-align: center;
align-items: center;
justify-content: center;
}
}
.actions {
.icon {
font-size: 14px;
}
}
.status-indicator {
.error {
color: @term-red;
}
.success {
color: @term-green;
}
.output {
color: @term-white;
}
}

View File

@ -0,0 +1,75 @@
import React from "react";
import { StatusIndicatorLevel } from "../../../types/types";
import cn from "classnames";
interface PositionalIconProps {
children?: React.ReactNode;
className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}
export class FrontIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div className={cn("front-icon", "positional-icon", this.props.className)}>
<div className="positional-icon-inner">{this.props.children}</div>
</div>
);
}
}
export class CenteredIcon extends React.Component<PositionalIconProps> {
render() {
return (
<div className={cn("centered-icon", "positional-icon", this.props.className)} onClick={this.props.onClick}>
<div className="positional-icon-inner">{this.props.children}</div>
</div>
);
}
}
interface ActionsIconProps {
onClick: React.MouseEventHandler<HTMLDivElement>;
}
export class ActionsIcon extends React.Component<ActionsIconProps> {
render() {
return (
<CenteredIcon className="actions" onClick={this.props.onClick}>
<div className="icon hoverEffect fa-sharp fa-solid fa-1x fa-ellipsis-vertical"></div>
</CenteredIcon>
);
}
}
interface StatusIndicatorProps {
level: StatusIndicatorLevel;
className?: string;
}
export class StatusIndicator extends React.Component<StatusIndicatorProps> {
render() {
const statusIndicatorLevel = this.props.level;
let statusIndicator = null;
if (statusIndicatorLevel != StatusIndicatorLevel.None) {
let statusIndicatorClass = null;
switch (statusIndicatorLevel) {
case StatusIndicatorLevel.Output:
statusIndicatorClass = "output";
break;
case StatusIndicatorLevel.Success:
statusIndicatorClass = "success";
break;
case StatusIndicatorLevel.Error:
statusIndicatorClass = "error";
break;
}
statusIndicator = (
<CenteredIcon className={cn(this.props.className, "status-indicator")}>
<div className={cn(statusIndicatorClass, "fa-sharp", "fa-solid", "fa-circle-small")}></div>
</CenteredIcon>
);
}
return statusIndicator;
}
}

View File

@ -1,4 +1,5 @@
@import "../../app/common/themes/themes.less"; @import "../../app/common/themes/themes.less";
@import "../../app/common/icons/icons.less";
.main-sidebar { .main-sidebar {
padding: 0; padding: 0;
@ -23,6 +24,7 @@
&.collapsed { &.collapsed {
width: 6em; width: 6em;
min-width: 6em; min-width: 6em;
.arrow-container, .arrow-container,
.collapse-button { .collapse-button {
transform: rotate(180deg); transform: rotate(180deg);
@ -125,12 +127,9 @@
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset; 0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
} }
.index { .index {
margin: 0 1.5em 0 -0.5rem;
font-size: 0.8em; font-size: 0.8em;
vertical-align: middle; text-align: center;
text-align: right;
width: 2em; width: 2em;
display: inline-block;
} }
.hotkey { .hotkey {
float: left !important; float: left !important;
@ -150,7 +149,7 @@
.item { .item {
padding: 5px; padding: 5px;
margin: 0 6px; margin-left: 6px;
border-radius: 4px; border-radius: 4px;
opacity: 1; opacity: 1;
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end; transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
@ -161,50 +160,43 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
.front-icon {
.positional-icon-visible;
}
.end-icons {
height: 20px;
}
.item-contents { .item-contents {
flex-grow: 1; flex-grow: 1;
} }
.icon { .icon {
margin: -2px 8px 0px 4px;
width: 16px; width: 16px;
height: 16px; height: 16px;
display: inline-block;
vertical-align: middle;
}
.actions.icon {
margin-left: 8px;
} }
.hotkey { .hotkey {
float: right; float: right;
margin-right: 6px; margin-right: 6px;
display: none;
letter-spacing: 6px; letter-spacing: 6px;
} }
.disabled .hotkey {
display: none;
}
.actions {
display: none;
}
&:hover { &:hover {
.hotkey { :not(.disabled) .hotkey {
display: block; .positional-icon-visible;
} }
.actions { .actions {
display: block; .positional-icon-visible;
}
.status-indicator {
display: none;
} }
} }
&:not(:hover) .status-indicator {
.positional-icon-visible;
}
.add_workspace { .add_workspace {
float: right; float: right;
width: 1.5rem;
height: 1.5rem;
padding: 2px; padding: 2px;
margin-right: 6px; margin-right: 6px;
transition: transform 0.3s ease-in-out; transition: transform 0.3s ease-in-out;
vertical-align: middle;
svg { svg {
fill: @base-color; fill: @base-color;
} }

View File

@ -15,6 +15,7 @@ import { ReactComponent as LeftChevronIcon } from "../assets/icons/chevron_left.
import { ReactComponent as AppsIcon } from "../assets/icons/apps.svg"; import { ReactComponent as AppsIcon } from "../assets/icons/apps.svg";
import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg"; import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg";
import { ReactComponent as AddIcon } from "../assets/icons/add.svg"; import { ReactComponent as AddIcon } from "../assets/icons/add.svg";
import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model"; import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
@ -22,7 +23,7 @@ import { isBlank, openLink } from "../../util/util";
import * as constants from "../appconst"; import * as constants from "../appconst";
import "./sidebar.less"; import "./sidebar.less";
import { ActionsIcon, EndIcon, FrontIcon, StatusIndicator } from "../common/common"; import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common/icons/icons";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@ -31,26 +32,34 @@ type OV<V> = mobx.IObservableValue<V>;
class SideBarItem extends React.Component<{ class SideBarItem extends React.Component<{
frontIcon: React.ReactNode; frontIcon: React.ReactNode;
contents: React.ReactNode | string; contents: React.ReactNode | string;
endIcon?: React.ReactNode[]; endIcons?: React.ReactNode[];
className?: string; className?: string;
key?: React.Key;
onClick?: React.MouseEventHandler<HTMLDivElement>; onClick?: React.MouseEventHandler<HTMLDivElement>;
}> { }> {
render() { render() {
return ( return (
<div <div
key={this.props.key}
className={cn("item", "unselectable", "hoverEffect", this.props.className)} className={cn("item", "unselectable", "hoverEffect", this.props.className)}
onClick={this.props.onClick} onClick={this.props.onClick}
> >
<FrontIcon>{this.props.frontIcon}</FrontIcon> <FrontIcon>{this.props.frontIcon}</FrontIcon>
<div className="item-contents truncate">{this.props.contents}</div> <div className="item-contents truncate">{this.props.contents}</div>
<EndIcon>{this.props.endIcon}</EndIcon> <div className="end-icons">{this.props.endIcons}</div>
</div> </div>
); );
} }
} }
class HotKeyIcon extends React.Component<{ hotkey: string }> {
render() {
return (
<CenteredIcon className="hotkey">
<span>&#x2318;{this.props.hotkey}</span>
</CenteredIcon>
);
}
}
@mobxReact.observer @mobxReact.observer
class MainSideBar extends React.Component<{}, {}> { class MainSideBar extends React.Component<{}, {}> {
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false); collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false);
@ -180,15 +189,12 @@ class MainSideBar extends React.Component<{}, {}> {
const sessionIndicator = Math.max(...sessionScreens.map((screen) => screen.statusIndicator.get())); const sessionIndicator = Math.max(...sessionScreens.map((screen) => screen.statusIndicator.get()));
return ( return (
<SideBarItem <SideBarItem
key={index}
className={`${isActive ? "active" : ""}`} className={`${isActive ? "active" : ""}`}
frontIcon={<span className="index">{index + 1}</span>} frontIcon={<span className="index">{index + 1}</span>}
contents={session.name.get()} contents={session.name.get()}
endIcon={[ endIcons={[
<StatusIndicator level={sessionIndicator} />, <StatusIndicator key="statusindicator" level={sessionIndicator} />,
<ActionsIcon <ActionsIcon key="actions" onClick={(e) => this.openSessionSettings(e, session)} />,
onClick={(e) => this.openSessionSettings(e, session)}
/>,
]} ]}
onClick={() => this.handleSessionClick(session.sessionId)} onClick={() => this.handleSessionClick(session.sessionId)}
/> />
@ -228,12 +234,12 @@ class MainSideBar extends React.Component<{}, {}> {
<SideBarItem <SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />} frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
contents="History" contents="History"
endIcon={[<span className="hotkey">&#x2318;H</span>]} endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
onClick={this.handleHistoryClick} onClick={this.handleHistoryClick}
/> />
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">&#x2318;B</span>} onClick={this.handleBookmarksClick}/> */} {/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">&#x2318;B</span>} onClick={this.handleBookmarksClick}/> */}
<SideBarItem <SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-globe icon "/>} frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
contents="Connections" contents="Connections"
onClick={this.handleConnectionsClick} onClick={this.handleConnectionsClick}
/> />
@ -242,8 +248,12 @@ class MainSideBar extends React.Component<{}, {}> {
<SideBarItem <SideBarItem
frontIcon={<WorkspacesIcon className="icon" />} frontIcon={<WorkspacesIcon className="icon" />}
contents="Workspaces" contents="Workspaces"
endIcon={[ endIcons={[
<div className="add_workspace hoverEffect" onClick={this.handleNewSession}> <div
key="add_workspace"
className="add_workspace hoverEffect"
onClick={this.handleNewSession}
>
<AddIcon /> <AddIcon />
</div>, </div>,
]} ]}
@ -263,11 +273,11 @@ class MainSideBar extends React.Component<{}, {}> {
frontIcon={<AppsIcon className="icon" />} frontIcon={<AppsIcon className="icon" />}
contents="Apps" contents="Apps"
onClick={this.handlePluginsClick} onClick={this.handlePluginsClick}
endIcon={[<span className="hotkey">&#x2318;A</span>]} endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
/> />
</If> </If>
<SideBarItem <SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-gear icon"/>} frontIcon={<SettingsIcon className="icon" />}
contents="Settings" contents="Settings"
onClick={this.handleSettingsClick} onClick={this.handleSettingsClick}
/> />

View File

@ -7,7 +7,8 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import cn from "classnames"; import cn from "classnames";
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model"; import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
import { ActionsIcon, EndIcon, StatusIndicator, renderCmdText } from "../../common/common"; import { ActionsIcon, StatusIndicator, CenteredIcon } from "../../common/icons/icons";
import { renderCmdText } from "../../common/common";
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg"; import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
import * as constants from "../../appconst"; import * as constants from "../../appconst";
import { Reorder } from "framer-motion"; import { Reorder } from "framer-motion";
@ -79,7 +80,11 @@ class ScreenTab extends React.Component<
let tabIndex = null; let tabIndex = null;
if (index + 1 <= 9) { if (index + 1 <= 9) {
tabIndex = <div className="tab-index">{renderCmdText(String(index + 1))}</div>; tabIndex = (
<CenteredIcon className="tab-index">
<div>{renderCmdText(String(index + 1))}</div>
</CenteredIcon>
);
} }
let archived = screen.archived.get() ? ( let archived = screen.archived.get() ? (
<i title="archived" className="fa-sharp fa-solid fa-box-archive" /> <i title="archived" className="fa-sharp fa-solid fa-box-archive" />
@ -109,19 +114,17 @@ class ScreenTab extends React.Component<
onContextMenu={(event) => this.openScreenSettings(event, screen)} onContextMenu={(event) => this.openScreenSettings(event, screen)}
onDragEnd={this.handleDragEnd} onDragEnd={this.handleDragEnd}
> >
<div className="front-icon"> <CenteredIcon className="front-icon">{this.renderTabIcon(screen)}</CenteredIcon>
{this.renderTabIcon(screen)}
</div>
<div className="tab-name truncate"> <div className="tab-name truncate">
{archived} {archived}
{webShared} {webShared}
{screen.name.get()} {screen.name.get()}
</div> </div>
<EndIcon> <div className="end-icons">
<StatusIndicator level={statusIndicatorLevel}/> <StatusIndicator level={statusIndicatorLevel} />
{tabIndex} {tabIndex}
<ActionsIcon onClick={(e) => this.openScreenSettings(e, screen)} /> <ActionsIcon onClick={(e) => this.openScreenSettings(e, screen)} />
</EndIcon> </div>
</Reorder.Item> </Reorder.Item>
); );
} }

View File

@ -1,4 +1,5 @@
@import "../../../app/common/themes/themes.less"; @import "../../../app/common/themes/themes.less";
@import "../../../app/common/icons/icons.less";
#main .screen-tabs .screen-tab { #main .screen-tabs .screen-tab {
border-top: 1px solid transparent; border-top: 1px solid transparent;
@ -238,7 +239,6 @@
.screen-tabs-container-inner { .screen-tabs-container-inner {
overflow-x: scroll; overflow-x: scroll;
} }
.screen-tabs { .screen-tabs {
@ -257,14 +257,7 @@
padding: 0 8px 0 8px; padding: 0 8px 0 8px;
.front-icon { .front-icon {
margin-right: 5px; .positional-icon-visible;
.svg-icon svg {
width: 14px;
height: 14px;
}
.fa-icon {
font-size: 16px;
}
} }
.tab-name { .tab-name {
@ -283,40 +276,27 @@
} }
// Only one of these will be visible at a time // Only one of these will be visible at a time
.end-icon { .end-icons {
// This adjusts the position of the icon to account for the default 8px margin on the parent. We want the positional calculations for this icon to assume it is flush with the edge of the screen tab. // This adjusts the position of the icon to account for the default 8px margin on the parent. We want the positional calculations for this icon to assume it is flush with the edge of the screen tab.
margin: 0 -8px 0 0; margin: 0 -8px 0 0;
.status-indicator {
display: block;
}
.actions {
display: none;
}
.tab-index { .tab-index {
display: none;
font-size: 12.5px; font-size: 12.5px;
} }
} }
&:hover {
.actions {
display: block;
}
}
} }
&:hover .screen-tab { &:not(:hover) .status-indicator{
.tab-index { .positional-icon-visible;
display: block;
}
.status-indicator {
display: none;
}
} }
&:hover .screen-tab:hover .tab-index { &:hover {
display: none; .screen-tab:not(:hover) .tab-index {
.positional-icon-visible;
}
.screen-tab:hover .actions {
.positional-icon-visible;
}
} }
} }