mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Split components in common (#276)
* split settings modals * init * init * remove styles not related to checkbox * remove styles not related to CmdStrCode * renderCmdText doesn't need styles * remove styles not related to dropdown * IconButton doesn't need styles because it extends Button * remove old connections * InfoMessage conmponent no longer needed * fix import error * remove styles not related to InlineSettingsTextEdit * remove styles not related to InputDecoration * LinkButton doesn't need styles cos it's extends Button component * remove styles not related to markdown * remove styles not related to modal * NumberField doesn't need styles cos it extends TextField * remove styles not related to PasswordField * RemoteStatusLight no longer used. It's replaced by Status component. * remove styles not related to ResizableSidebar * SettingsError doesn't need styles cos it uses classnames in app.less * remove styles not related to Status * remove styles not related to TextField * remove styles not related to Toggle * remove styles not related to Tooltip
This commit is contained in:
parent
fc65e65f11
commit
bf1b556537
@ -9,7 +9,7 @@ import { If, For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import type { BookmarkType } from "../../types/types";
|
||||
import { GlobalModel } from "../../model/model";
|
||||
import { CmdStrCode, Markdown } from "../common/common";
|
||||
import { CmdStrCode, Markdown } from "../common/elements";
|
||||
|
||||
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
|
||||
import { ReactComponent as CopyIcon } from "../assets/icons/favourites/copy.svg";
|
||||
|
@ -7,7 +7,7 @@ import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner, MinFontSize, MaxFontSize, RemotesModel } from "../../model/model";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "../common/common";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "../common/elements";
|
||||
import { CommandRtnType, ClientDataType } from "../../types/types";
|
||||
import { commandRtnHandler, isBlank } from "../../util/util";
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
123
src/app/common/elements/button.less
Normal file
123
src/app/common/elements/button.less
Normal file
@ -0,0 +1,123 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-button {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
|
||||
display: flex;
|
||||
padding: 6px 16px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 6px;
|
||||
height: auto;
|
||||
|
||||
&:hover {
|
||||
color: @term-white;
|
||||
}
|
||||
|
||||
i {
|
||||
fill: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: @term-green;
|
||||
background: none;
|
||||
|
||||
i {
|
||||
fill: @term-green;
|
||||
}
|
||||
|
||||
&.solid {
|
||||
color: @term-bright-white;
|
||||
background: @term-green;
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset;
|
||||
|
||||
i {
|
||||
fill: @term-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
border: 1px solid @term-green;
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
// Styles for .ghost are already defined above
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: @term-bright-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: @term-white;
|
||||
background: none;
|
||||
|
||||
&.solid {
|
||||
background: rgba(255, 255, 255, 0.09);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
padding: 6px 10px;
|
||||
|
||||
i {
|
||||
fill: @term-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.color-yellow {
|
||||
&.solid {
|
||||
border-color: @warning-yellow;
|
||||
background-color: mix(@warning-yellow, @term-white, 50%);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
color: @warning-yellow;
|
||||
border-color: @warning-yellow;
|
||||
&:hover {
|
||||
color: @term-white;
|
||||
border-color: @term-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
}
|
||||
}
|
||||
|
||||
&.color-red {
|
||||
&.solid {
|
||||
border-color: @term-red;
|
||||
background-color: mix(@term-red, @term-white, 50%);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
color: @term-red;
|
||||
border-color: @term-red;
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.link-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
63
src/app/common/elements/button.tsx
Normal file
63
src/app/common/elements/button.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./button.less";
|
||||
|
||||
type ButtonVariantType = "outlined" | "solid" | "ghost";
|
||||
type ButtonThemeType = "primary" | "secondary";
|
||||
|
||||
interface ButtonProps {
|
||||
theme?: ButtonThemeType;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
variant?: ButtonVariantType;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
color?: string;
|
||||
style?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
class Button extends React.Component<ButtonProps> {
|
||||
static defaultProps = {
|
||||
theme: "primary",
|
||||
variant: "solid",
|
||||
color: "",
|
||||
style: {},
|
||||
};
|
||||
|
||||
@boundMethod
|
||||
handleClick() {
|
||||
if (this.props.onClick && !this.props.disabled) {
|
||||
this.props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { leftIcon, rightIcon, theme, children, disabled, variant, color, style, autoFocus, className } =
|
||||
this.props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn("wave-button", theme, variant, color, { disabled: disabled }, className)}
|
||||
onClick={this.handleClick}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
autoFocus={autoFocus}
|
||||
>
|
||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Button };
|
||||
export type { ButtonProps };
|
68
src/app/common/elements/checkbox.less
Normal file
68
src/app/common/elements/checkbox.less
Normal file
@ -0,0 +1,68 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
|
||||
input[type="checkbox"] {
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
input[type="checkbox"] + label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: @term-bright-white;
|
||||
transition: color 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||
}
|
||||
input[type="checkbox"] + label > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
border: 2px solid #9e9e9e;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||
}
|
||||
|
||||
input[type="checkbox"] + label:hover > span,
|
||||
input[type="checkbox"]:focus + label > span {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
input[type="checkbox"]:checked + label > ins {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked + label > span {
|
||||
border: 10px solid @term-green;
|
||||
}
|
||||
input[type="checkbox"]:checked + label > span:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 3px;
|
||||
width: 7px;
|
||||
height: 12px;
|
||||
border-right: 2px solid #fff;
|
||||
border-bottom: 2px solid #fff;
|
||||
transform: rotate(45deg);
|
||||
transform-origin: 0% 100%;
|
||||
animation: checkbox-check 500ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||
}
|
||||
|
||||
@keyframes checkbox-check {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
33% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
70
src/app/common/elements/checkbox.tsx
Normal file
70
src/app/common/elements/checkbox.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobx from "mobx";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./checkbox.less";
|
||||
|
||||
class Checkbox extends React.Component<
|
||||
{
|
||||
checked?: boolean;
|
||||
defaultChecked?: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label: React.ReactNode;
|
||||
className?: string;
|
||||
id?: string;
|
||||
},
|
||||
{ checkedInternal: boolean }
|
||||
> {
|
||||
generatedId;
|
||||
static idCounter = 0;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
checkedInternal: this.props.checked ?? Boolean(this.props.defaultChecked),
|
||||
};
|
||||
this.generatedId = `checkbox-${Checkbox.idCounter++}`;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.checked !== undefined && this.props.checked !== prevProps.checked) {
|
||||
this.setState({ checkedInternal: this.props.checked });
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
const newChecked = e.target.checked;
|
||||
if (this.props.checked === undefined) {
|
||||
this.setState({ checkedInternal: newChecked });
|
||||
}
|
||||
this.props.onChange(newChecked);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, className, id } = this.props;
|
||||
const { checkedInternal } = this.state;
|
||||
const checkboxId = id || this.generatedId;
|
||||
|
||||
return (
|
||||
<div className={cn("checkbox", className)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={checkboxId}
|
||||
checked={checkedInternal}
|
||||
onChange={this.handleChange}
|
||||
aria-checked={checkedInternal}
|
||||
role="checkbox"
|
||||
/>
|
||||
<label htmlFor={checkboxId}>
|
||||
<span></span>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Checkbox };
|
102
src/app/common/elements/cmdstrcode.less
Normal file
102
src/app/common/elements/cmdstrcode.less
Normal file
@ -0,0 +1,102 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.cmdstr-code {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0px 10px 0px 0;
|
||||
|
||||
&.is-large {
|
||||
.use-button {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.code-div code {
|
||||
}
|
||||
}
|
||||
|
||||
&.limit-height .code-div {
|
||||
max-height: 58px;
|
||||
}
|
||||
|
||||
&.limit-height.is-large .code-div {
|
||||
max-height: 68px;
|
||||
}
|
||||
|
||||
.use-button {
|
||||
flex-grow: 0;
|
||||
padding: 3px;
|
||||
border-radius: 3px 0 0 3px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: flex-start;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.code-div {
|
||||
background-color: @term-black;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 100px;
|
||||
overflow: auto;
|
||||
border-left: 1px solid #777;
|
||||
|
||||
code {
|
||||
flex-shrink: 0;
|
||||
min-width: 100px;
|
||||
color: @term-white;
|
||||
white-space: pre;
|
||||
padding: 2px 8px 2px 8px;
|
||||
background-color: @term-black;
|
||||
font-size: 1em;
|
||||
font-family: @fixed-font;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-control {
|
||||
width: 0;
|
||||
position: relative;
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
|
||||
.inner-copy {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
right: -20px;
|
||||
|
||||
padding: 2px;
|
||||
padding-left: 4px;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
|
||||
&:hover {
|
||||
color: @term-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .copy-control {
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.copied-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: @term-white;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
animation-name: fade-in-out;
|
||||
animation-duration: 0.3s;
|
||||
}
|
66
src/app/common/elements/cmdstrcode.tsx
Normal file
66
src/app/common/elements/cmdstrcode.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
|
||||
import { ReactComponent as CheckIcon } from "../../assets/icons/line/check.svg";
|
||||
import { ReactComponent as CopyIcon } from "../../assets/icons/history/copy.svg";
|
||||
|
||||
import "./cmdstrcode.less";
|
||||
|
||||
class CmdStrCode extends React.Component<
|
||||
{
|
||||
cmdstr: string;
|
||||
onUse: () => void;
|
||||
onCopy: () => void;
|
||||
isCopied: boolean;
|
||||
fontSize: "normal" | "large";
|
||||
limitHeight: boolean;
|
||||
},
|
||||
{}
|
||||
> {
|
||||
@boundMethod
|
||||
handleUse(e: any) {
|
||||
e.stopPropagation();
|
||||
if (this.props.onUse != null) {
|
||||
this.props.onUse();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleCopy(e: any) {
|
||||
e.stopPropagation();
|
||||
if (this.props.onCopy != null) {
|
||||
this.props.onCopy();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let { isCopied, cmdstr, fontSize, limitHeight } = this.props;
|
||||
return (
|
||||
<div className={cn("cmdstr-code", { "is-large": fontSize == "large" }, { "limit-height": limitHeight })}>
|
||||
<If condition={isCopied}>
|
||||
<div key="copied" className="copied-indicator">
|
||||
<div>copied</div>
|
||||
</div>
|
||||
</If>
|
||||
<div key="use" className="use-button hoverEffect" title="Use Command" onClick={this.handleUse}>
|
||||
<CheckIcon className="icon" />
|
||||
</div>
|
||||
<div key="code" className="code-div">
|
||||
<code>{cmdstr}</code>
|
||||
</div>
|
||||
<div key="copy" className="copy-control hoverEffect">
|
||||
<div className="inner-copy" onClick={this.handleCopy} title="copy">
|
||||
<CopyIcon className="icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { CmdStrCode };
|
10
src/app/common/elements/cmdtext.tsx
Normal file
10
src/app/common/elements/cmdtext.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
function renderCmdText(text: string): any {
|
||||
return <span>⌘{text}</span>;
|
||||
}
|
||||
|
||||
export { renderCmdText };
|
127
src/app/common/elements/dropdown.less
Normal file
127
src/app/common/elements/dropdown.less
Normal file
@ -0,0 +1,127 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-dropdown {
|
||||
position: relative;
|
||||
height: 44px;
|
||||
min-width: 150px;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(241, 246, 243, 0.15);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
|
||||
&.no-label {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
&-label {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 16px;
|
||||
font-size: 12.5px;
|
||||
transition: all 0.3s;
|
||||
color: @term-white;
|
||||
line-height: 10px;
|
||||
|
||||
&.float {
|
||||
font-size: 10px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&.offset-left {
|
||||
left: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&-display {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
bottom: 5px;
|
||||
|
||||
&.offset-left {
|
||||
left: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: transform 0.3s;
|
||||
pointer-events: none;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&-arrow-rotate {
|
||||
transform: translateY(-50%) rotate(180deg); // Rotate the arrow when dropdown is open
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
min-width: 120px;
|
||||
padding: 5px 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
border-radius: 6px;
|
||||
|
||||
&-highlighted,
|
||||
&:hover {
|
||||
background: rgba(241, 246, 243, 0.08);
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-input-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wave-input-decoration.end-position {
|
||||
margin-right: 44px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.wave-input-decoration.start-position {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&-error {
|
||||
border-color: @term-red;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: @term-green;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 2px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
border-radius: 6px;
|
||||
background: #151715;
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5), 0px 3px 8px 0px rgba(0, 0, 0, 0.35), 0px 0px 0.5px 0px #fff inset,
|
||||
0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
animation-fill-mode: forwards;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.wave-dropdown-menu-close {
|
||||
z-index: 0;
|
||||
}
|
259
src/app/common/elements/dropdown.tsx
Normal file
259
src/app/common/elements/dropdown.tsx
Normal file
@ -0,0 +1,259 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import "./dropdown.less";
|
||||
|
||||
interface DropdownDecorationProps {
|
||||
startDecoration?: React.ReactNode;
|
||||
endDecoration?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
label?: string;
|
||||
options: { value: string; label: string }[];
|
||||
value?: string;
|
||||
className?: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
decoration?: DropdownDecorationProps;
|
||||
defaultValue?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
interface DropdownState {
|
||||
isOpen: boolean;
|
||||
internalValue: string;
|
||||
highlightedIndex: number;
|
||||
isTouched: boolean;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
||||
wrapperRef: React.RefObject<HTMLDivElement>;
|
||||
menuRef: React.RefObject<HTMLDivElement>;
|
||||
timeoutId: any;
|
||||
|
||||
constructor(props: DropdownProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
internalValue: props.defaultValue || "",
|
||||
highlightedIndex: -1,
|
||||
isTouched: false,
|
||||
};
|
||||
this.wrapperRef = React.createRef();
|
||||
this.menuRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener("mousedown", this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("mousedown", this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<DropdownProps>, prevState: Readonly<DropdownState>, snapshot?: any): void {
|
||||
// If the dropdown was open but now is closed, start the timeout
|
||||
if (prevState.isOpen && !this.state.isOpen) {
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (this.menuRef.current) {
|
||||
this.menuRef.current.style.display = "none";
|
||||
}
|
||||
}, 300); // Time is equal to the animation duration
|
||||
}
|
||||
// If the dropdown is now open, cancel any existing timeout and show the menu
|
||||
else if (!prevState.isOpen && this.state.isOpen) {
|
||||
if (this.timeoutId !== null) {
|
||||
clearTimeout(this.timeoutId); // Cancel any existing timeout
|
||||
this.timeoutId = null;
|
||||
}
|
||||
if (this.menuRef.current) {
|
||||
this.menuRef.current.style.display = "inline-flex";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleClickOutside(event: MouseEvent) {
|
||||
// Check if the click is outside both the wrapper and the menu
|
||||
if (
|
||||
this.wrapperRef.current &&
|
||||
!this.wrapperRef.current.contains(event.target as Node) &&
|
||||
this.menuRef.current &&
|
||||
!this.menuRef.current.contains(event.target as Node)
|
||||
) {
|
||||
this.setState({ isOpen: false });
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleClick() {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleFocus() {
|
||||
this.setState({ isTouched: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleKeyDown(event: React.KeyboardEvent) {
|
||||
const { options } = this.props;
|
||||
const { isOpen, highlightedIndex } = this.state;
|
||||
|
||||
switch (event.key) {
|
||||
case "Enter":
|
||||
case " ":
|
||||
if (isOpen) {
|
||||
const option = options[highlightedIndex];
|
||||
if (option) {
|
||||
this.handleSelect(option.value, undefined);
|
||||
}
|
||||
} else {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
this.setState({ isOpen: false });
|
||||
break;
|
||||
case "ArrowUp":
|
||||
if (isOpen) {
|
||||
this.setState((prevState) => ({
|
||||
highlightedIndex:
|
||||
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case "ArrowDown":
|
||||
if (isOpen) {
|
||||
this.setState((prevState) => ({
|
||||
highlightedIndex:
|
||||
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case "Tab":
|
||||
this.setState({ isOpen: false });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) {
|
||||
const { onChange } = this.props;
|
||||
if (event) {
|
||||
event.stopPropagation(); // This stops the event from bubbling up to the wrapper
|
||||
}
|
||||
|
||||
if (!("value" in this.props)) {
|
||||
this.setState({ internalValue: value });
|
||||
}
|
||||
onChange(value);
|
||||
this.setState({ isOpen: false, isTouched: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
toggleDropdown() {
|
||||
this.setState((prevState) => ({ isOpen: !prevState.isOpen, isTouched: true }));
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
calculatePosition(): React.CSSProperties {
|
||||
if (this.wrapperRef.current) {
|
||||
const rect = this.wrapperRef.current.getBoundingClientRect();
|
||||
return {
|
||||
position: "absolute",
|
||||
top: `${rect.bottom + window.scrollY}px`,
|
||||
left: `${rect.left + window.scrollX}px`,
|
||||
width: `${rect.width}px`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, options, value, placeholder, decoration, className, required } = this.props;
|
||||
const { isOpen, internalValue, highlightedIndex, isTouched } = this.state;
|
||||
|
||||
const currentValue = value ?? internalValue;
|
||||
const selectedOptionLabel =
|
||||
options.find((option) => option.value === currentValue)?.label || placeholder || internalValue;
|
||||
|
||||
// Determine if the dropdown should be marked as having an error
|
||||
const isError =
|
||||
required &&
|
||||
(value === undefined || value === "") &&
|
||||
(internalValue === undefined || internalValue === "") &&
|
||||
isTouched;
|
||||
|
||||
// Determine if the label should float
|
||||
const shouldLabelFloat = !!value || !!internalValue || !!placeholder || isOpen;
|
||||
|
||||
const dropdownMenu = isOpen
|
||||
? ReactDOM.createPortal(
|
||||
<div className={cn("wave-dropdown-menu")} ref={this.menuRef} style={this.calculatePosition()}>
|
||||
{options.map((option, index) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn("wave-dropdown-item unselectable", {
|
||||
"wave-dropdown-item-highlighted": index === highlightedIndex,
|
||||
})}
|
||||
onClick={(e) => this.handleSelect(option.value, e)}
|
||||
onMouseEnter={() => this.setState({ highlightedIndex: index })}
|
||||
onMouseLeave={() => this.setState({ highlightedIndex: -1 })}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>,
|
||||
document.getElementById("app")!
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("wave-dropdown", className, {
|
||||
"wave-dropdown-error": isError,
|
||||
"no-label": !label,
|
||||
})}
|
||||
ref={this.wrapperRef}
|
||||
tabIndex={0}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onClick={this.handleClick}
|
||||
onFocus={this.handleFocus}
|
||||
>
|
||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||
<If condition={label}>
|
||||
<div
|
||||
className={cn("wave-dropdown-label unselectable", {
|
||||
float: shouldLabelFloat,
|
||||
"offset-left": decoration?.startDecoration,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</If>
|
||||
<div
|
||||
className={cn("wave-dropdown-display unselectable", { "offset-left": decoration?.startDecoration })}
|
||||
>
|
||||
{selectedOptionLabel}
|
||||
</div>
|
||||
<div className={cn("wave-dropdown-arrow", { "wave-dropdown-arrow-rotate": isOpen })}>
|
||||
<i className="fa-sharp fa-solid fa-chevron-down"></i>
|
||||
</div>
|
||||
{dropdownMenu}
|
||||
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Dropdown };
|
21
src/app/common/elements/iconbutton.tsx
Normal file
21
src/app/common/elements/iconbutton.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { Button } from "./button";
|
||||
class IconButton extends Button {
|
||||
render() {
|
||||
const { children, theme, variant = "solid", ...rest } = this.props;
|
||||
const className = `wave-button icon-button ${theme} ${variant}`;
|
||||
|
||||
return (
|
||||
<button {...rest} className={className}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default IconButton;
|
||||
|
||||
export { IconButton };
|
20
src/app/common/elements/index.tsx
Normal file
20
src/app/common/elements/index.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
export { Button } from "./button";
|
||||
export { Checkbox } from "./checkbox";
|
||||
export { CmdStrCode } from "./cmdstrcode";
|
||||
export { renderCmdText } from "./cmdtext";
|
||||
export { Dropdown } from "./dropdown";
|
||||
export { IconButton } from "./iconbutton";
|
||||
export { InlineSettingsTextEdit } from "./inlinesettingstextedit";
|
||||
export { InputDecoration } from "./inputdecoration";
|
||||
export { LinkButton } from "./linkbutton";
|
||||
export { Markdown } from "./markdown";
|
||||
export { Modal } from "./modal";
|
||||
export { NumberField } from "./numberfield";
|
||||
export { PasswordField } from "./passwordfield";
|
||||
export { ResizableSidebar } from "./resizablesidebar";
|
||||
export { SettingsError } from "./settingserror";
|
||||
export { ShowWaveShellInstallPrompt } from "./showwaveshellinstallprompt";
|
||||
export { Status } from "./status";
|
||||
export { TextField } from "./textfield";
|
||||
export { Toggle } from "./toggle";
|
||||
export { Tooltip } from "./tooltip";
|
40
src/app/common/elements/inlinesettingstextedit.less
Normal file
40
src/app/common/elements/inlinesettingstextedit.less
Normal file
@ -0,0 +1,40 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.inline-edit {
|
||||
.icon {
|
||||
display: inline;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 1em;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&.edit-not-active {
|
||||
cursor: pointer;
|
||||
|
||||
i.fa-pen {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
}
|
||||
}
|
||||
|
||||
&.edit-active {
|
||||
input.input {
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
149
src/app/common/elements/inlinesettingstextedit.tsx
Normal file
149
src/app/common/elements/inlinesettingstextedit.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
|
||||
|
||||
import "./inlinesettingstextedit.less";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
@mobxReact.observer
|
||||
class InlineSettingsTextEdit extends React.Component<
|
||||
{
|
||||
text: string;
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
maxLength: number;
|
||||
placeholder: string;
|
||||
showIcon?: boolean;
|
||||
},
|
||||
{}
|
||||
> {
|
||||
isEditing: OV<boolean> = mobx.observable.box(false, { name: "inlineedit-isEditing" });
|
||||
tempText: OV<string>;
|
||||
shouldFocus: boolean = false;
|
||||
inputRef: React.RefObject<any> = React.createRef();
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if (this.shouldFocus) {
|
||||
this.shouldFocus = false;
|
||||
if (this.inputRef.current != null) {
|
||||
this.inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleChangeText(e: any): void {
|
||||
mobx.action(() => {
|
||||
this.tempText.set(e.target.value);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
confirmChange(): void {
|
||||
mobx.action(() => {
|
||||
let newText = this.tempText.get();
|
||||
this.isEditing.set(false);
|
||||
this.tempText = null;
|
||||
this.props.onChange(newText);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
cancelChange(): void {
|
||||
mobx.action(() => {
|
||||
this.isEditing.set(false);
|
||||
this.tempText = null;
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleKeyDown(e: any): void {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.confirmChange();
|
||||
return;
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.cancelChange();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickEdit(): void {
|
||||
mobx.action(() => {
|
||||
this.isEditing.set(true);
|
||||
this.shouldFocus = true;
|
||||
this.tempText = mobx.observable.box(this.props.value, { name: "inlineedit-tempText" });
|
||||
})();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.isEditing.get()) {
|
||||
return (
|
||||
<div className={cn("settings-input inline-edit", "edit-active")}>
|
||||
<div className="field has-addons">
|
||||
<div className="control">
|
||||
<input
|
||||
ref={this.inputRef}
|
||||
className="input"
|
||||
type="text"
|
||||
onKeyDown={this.handleKeyDown}
|
||||
placeholder={this.props.placeholder}
|
||||
onChange={this.handleChangeText}
|
||||
value={this.tempText.get()}
|
||||
maxLength={this.props.maxLength}
|
||||
/>
|
||||
</div>
|
||||
<div className="control">
|
||||
<div
|
||||
onClick={this.cancelChange}
|
||||
title="Cancel (Esc)"
|
||||
className="button is-prompt-danger is-outlined is-small"
|
||||
>
|
||||
<span className="icon is-small">
|
||||
<i className="fa-sharp fa-solid fa-xmark" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="control">
|
||||
<div
|
||||
onClick={this.confirmChange}
|
||||
title="Confirm (Enter)"
|
||||
className="button is-wave-green is-outlined is-small"
|
||||
>
|
||||
<span className="icon is-small">
|
||||
<i className="fa-sharp fa-solid fa-check" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div onClick={this.clickEdit} className={cn("settings-input inline-edit", "edit-not-active")}>
|
||||
{this.props.text}
|
||||
<If condition={this.props.showIcon}>
|
||||
<i className="fa-sharp fa-solid fa-pen" />
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { InlineSettingsTextEdit };
|
19
src/app/common/elements/inputdecoration.less
Normal file
19
src/app/common/elements/inputdecoration.less
Normal file
@ -0,0 +1,19 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-input-decoration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
i {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-input-decoration.start-position {
|
||||
margin: 0 4px 0 16px;
|
||||
}
|
||||
|
||||
.wave-input-decoration.end-position {
|
||||
margin: 0 16px 0 8px;
|
||||
}
|
32
src/app/common/elements/inputdecoration.tsx
Normal file
32
src/app/common/elements/inputdecoration.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./inputdecoration.less";
|
||||
|
||||
interface InputDecorationProps {
|
||||
position?: "start" | "end";
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class InputDecoration extends React.Component<InputDecorationProps, {}> {
|
||||
render() {
|
||||
const { children, position = "end" } = this.props;
|
||||
return (
|
||||
<div
|
||||
className={cn("wave-input-decoration", {
|
||||
"start-position": position === "start",
|
||||
"end-position": position === "end",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { InputDecoration };
|
28
src/app/common/elements/linkbutton.tsx
Normal file
28
src/app/common/elements/linkbutton.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import cn from "classnames";
|
||||
import { ButtonProps } from "./button";
|
||||
|
||||
interface LinkButtonProps extends ButtonProps {
|
||||
href: string;
|
||||
rel?: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
class LinkButton extends React.Component<LinkButtonProps> {
|
||||
render() {
|
||||
const { leftIcon, rightIcon, children, className, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<a {...rest} className={cn(`wave-button link-button`, className)}>
|
||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||
{children}
|
||||
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { LinkButton };
|
91
src/app/common/elements/markdown.less
Normal file
91
src/app/common/elements/markdown.less
Normal file
@ -0,0 +1,91 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.markdown {
|
||||
color: @term-white;
|
||||
margin-bottom: 10px;
|
||||
font-family: @markdown-font;
|
||||
font-size: 14px;
|
||||
|
||||
code {
|
||||
background-color: @markdown-highlight;
|
||||
color: @term-white;
|
||||
font-family: @terminal-font;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
code.inline {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
font-family: @terminal-font;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: @term-white;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: @term-white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #32afff;
|
||||
}
|
||||
|
||||
table {
|
||||
tr th {
|
||||
color: @term-white;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: outside;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-position: outside;
|
||||
margin-left: 19px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 4px 10px 4px 10px;
|
||||
border-radius: 3px;
|
||||
background-color: @markdown-highlight;
|
||||
padding: 2px 4px 2px 6px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: @markdown-highlight;
|
||||
margin: 4px 10px 4px 10px;
|
||||
padding: 6px 6px 6px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre.selected {
|
||||
outline: 2px solid @term-green;
|
||||
}
|
||||
|
||||
.title.is-1 {
|
||||
border-bottom: 1px solid #777;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.title.is-2 {
|
||||
border-bottom: 1px solid #777;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.title.is-3 {
|
||||
}
|
||||
.title.is-4 {
|
||||
}
|
||||
.title.is-5 {
|
||||
}
|
||||
.title.is-6 {
|
||||
}
|
||||
}
|
||||
|
||||
.markdown > *:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
105
src/app/common/elements/markdown.tsx
Normal file
105
src/app/common/elements/markdown.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel } from "../../../model/model";
|
||||
|
||||
import "./markdown.less";
|
||||
|
||||
function LinkRenderer(props: any): any {
|
||||
let newUrl = "https://extern?" + encodeURIComponent(props.href);
|
||||
return (
|
||||
<a href={newUrl} target="_blank" rel={"noopener"}>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderRenderer(props: any, hnum: number): any {
|
||||
return <div className={cn("title", "is-" + hnum)}>{props.children}</div>;
|
||||
}
|
||||
|
||||
function CodeRenderer(props: any): any {
|
||||
return <code className={cn({ inline: props.inline })}>{props.children}</code>;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class CodeBlockMarkdown extends React.Component<{ children: React.ReactNode; codeSelectSelectedIndex?: number }, {}> {
|
||||
blockIndex: number;
|
||||
blockRef: React.RefObject<HTMLPreElement>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.blockRef = React.createRef();
|
||||
this.blockIndex = GlobalModel.inputModel.addCodeBlockToCodeSelect(this.blockRef);
|
||||
}
|
||||
|
||||
render() {
|
||||
let clickHandler: (e: React.MouseEvent<HTMLElement>, blockIndex: number) => void;
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
clickHandler = (e: React.MouseEvent<HTMLElement>, blockIndex: number) => {
|
||||
inputModel.setCodeSelectSelectedCodeBlock(blockIndex);
|
||||
};
|
||||
let selected = this.blockIndex == this.props.codeSelectSelectedIndex;
|
||||
return (
|
||||
<pre
|
||||
ref={this.blockRef}
|
||||
className={cn({ selected: selected })}
|
||||
onClick={(event) => clickHandler(event, this.blockIndex)}
|
||||
>
|
||||
{this.props.children}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class Markdown extends React.Component<
|
||||
{ text: string; style?: any; extraClassName?: string; codeSelect?: boolean },
|
||||
{}
|
||||
> {
|
||||
CodeBlockRenderer(props: any, codeSelect: boolean, codeSelectIndex: number): any {
|
||||
if (codeSelect) {
|
||||
return <CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex}>{props.children}</CodeBlockMarkdown>;
|
||||
} else {
|
||||
const clickHandler = (e: React.MouseEvent<HTMLElement>) => {
|
||||
let blockText = (e.target as HTMLElement).innerText;
|
||||
if (blockText) {
|
||||
blockText = blockText.replace(/\n$/, ""); // remove trailing newline
|
||||
navigator.clipboard.writeText(blockText);
|
||||
}
|
||||
};
|
||||
return <pre onClick={(event) => clickHandler(event)}>{props.children}</pre>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let text = this.props.text;
|
||||
let codeSelect = this.props.codeSelect;
|
||||
let curCodeSelectIndex = GlobalModel.inputModel.getCodeSelectSelectedIndex();
|
||||
let markdownComponents = {
|
||||
a: LinkRenderer,
|
||||
h1: (props) => HeaderRenderer(props, 1),
|
||||
h2: (props) => HeaderRenderer(props, 2),
|
||||
h3: (props) => HeaderRenderer(props, 3),
|
||||
h4: (props) => HeaderRenderer(props, 4),
|
||||
h5: (props) => HeaderRenderer(props, 5),
|
||||
h6: (props) => HeaderRenderer(props, 6),
|
||||
code: (props) => CodeRenderer(props),
|
||||
pre: (props) => this.CodeBlockRenderer(props, codeSelect, curCodeSelectIndex),
|
||||
};
|
||||
return (
|
||||
<div className={cn("markdown content", this.props.extraClassName)} style={this.props.style}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Markdown };
|
79
src/app/common/elements/modal.less
Normal file
79
src/app/common/elements/modal.less
Normal file
@ -0,0 +1,79 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-modal-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 500;
|
||||
|
||||
.wave-modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(21, 23, 21, 0.7);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-modal {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
border-radius: 10px;
|
||||
background: #151715;
|
||||
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45),
|
||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
|
||||
.wave-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.wave-modal-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 14px 12px 20px;
|
||||
justify-content: space-between;
|
||||
line-height: 20px;
|
||||
border-bottom: 1px solid rgba(250, 250, 250, 0.1);
|
||||
|
||||
.wave-modal-title {
|
||||
color: #eceeec;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
button {
|
||||
i {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wave-modal-body {
|
||||
width: 100%;
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
.wave-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
padding: 0 20px 20px;
|
||||
|
||||
button:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
81
src/app/common/elements/modal.tsx
Normal file
81
src/app/common/elements/modal.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobx from "mobx";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Button } from "./button";
|
||||
import { IconButton } from "./iconbutton";
|
||||
|
||||
import "./modal.less";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
interface ModalHeaderProps {
|
||||
onClose?: () => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ModalHeader: React.FC<ModalHeaderProps> = ({ onClose, title }) => (
|
||||
<div className="wave-modal-header">
|
||||
{<div className="wave-modal-title">{title}</div>}
|
||||
<If condition={onClose}>
|
||||
<IconButton theme="secondary" variant="ghost" onClick={onClose}>
|
||||
<i className="fa-sharp fa-solid fa-xmark"></i>
|
||||
</IconButton>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface ModalFooterProps {
|
||||
onCancel?: () => void;
|
||||
onOk?: () => void;
|
||||
cancelLabel?: string;
|
||||
okLabel?: string;
|
||||
}
|
||||
|
||||
const ModalFooter: React.FC<ModalFooterProps> = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => (
|
||||
<div className="wave-modal-footer">
|
||||
{onCancel && (
|
||||
<Button theme="secondary" onClick={onCancel}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
)}
|
||||
{onOk && <Button onClick={onOk}>{okLabel}</Button>}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface ModalProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
onClickBackdrop?: () => void;
|
||||
}
|
||||
|
||||
class Modal extends React.Component<ModalProps> {
|
||||
static Header = ModalHeader;
|
||||
static Footer = ModalFooter;
|
||||
|
||||
renderBackdrop(onClick: (() => void) | undefined) {
|
||||
return <div className="wave-modal-backdrop" onClick={onClick}></div>;
|
||||
}
|
||||
|
||||
renderModal() {
|
||||
const { className, children } = this.props;
|
||||
|
||||
return (
|
||||
<div className="wave-modal-container">
|
||||
{this.renderBackdrop(this.props.onClickBackdrop)}
|
||||
<div className={`wave-modal ${className}`}>
|
||||
<div className="wave-modal-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return ReactDOM.createPortal(this.renderModal(), document.getElementById("app"));
|
||||
}
|
||||
}
|
||||
|
||||
export { Modal };
|
39
src/app/common/elements/numberfield.tsx
Normal file
39
src/app/common/elements/numberfield.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
|
||||
import { TextField } from "./textfield";
|
||||
|
||||
class NumberField extends TextField {
|
||||
@boundMethod
|
||||
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { required, onChange } = this.props;
|
||||
const inputValue = e.target.value;
|
||||
|
||||
// Allow only numeric input
|
||||
if (inputValue === "" || /^\d*$/.test(inputValue)) {
|
||||
// Update the internal state only if the component is not controlled.
|
||||
if (this.props.value === undefined) {
|
||||
const isError = required ? inputValue.trim() === "" : false;
|
||||
|
||||
this.setState({
|
||||
internalValue: inputValue,
|
||||
error: isError,
|
||||
hasContent: Boolean(inputValue),
|
||||
});
|
||||
}
|
||||
|
||||
onChange && onChange(inputValue);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// Use the render method from TextField but add the onKeyDown handler
|
||||
const renderedTextField = super.render();
|
||||
return React.cloneElement(renderedTextField);
|
||||
}
|
||||
}
|
||||
|
||||
export { NumberField };
|
30
src/app/common/elements/passwordfield.less
Normal file
30
src/app/common/elements/passwordfield.less
Normal file
@ -0,0 +1,30 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-password {
|
||||
.wave-textfield-inner-eye {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 52%;
|
||||
transform: translateY(-50%);
|
||||
transition: transform 0.3s;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-input-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wave-input-decoration.end-position {
|
||||
margin-right: 47px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.wave-input-decoration.start-position {
|
||||
left: 0;
|
||||
}
|
||||
}
|
100
src/app/common/elements/passwordfield.tsx
Normal file
100
src/app/common/elements/passwordfield.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { TextFieldState, TextField } from "./textfield";
|
||||
|
||||
import "./passwordfield.less";
|
||||
|
||||
interface PasswordFieldState extends TextFieldState {
|
||||
passwordVisible: boolean;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class PasswordField extends TextField {
|
||||
state: PasswordFieldState;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...this.state,
|
||||
passwordVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
togglePasswordVisibility() {
|
||||
//@ts-ignore
|
||||
this.setState((prevState) => ({
|
||||
//@ts-ignore
|
||||
passwordVisible: !prevState.passwordVisible,
|
||||
}));
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
// Call the parent handleInputChange method
|
||||
super.handleInputChange(e);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { decoration, className, placeholder, maxLength, label } = this.props;
|
||||
const { focused, internalValue, error, passwordVisible } = this.state;
|
||||
const inputValue = this.props.value ?? internalValue;
|
||||
|
||||
// The input should always receive the real value
|
||||
const inputProps = {
|
||||
className: cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration }),
|
||||
ref: this.inputRef,
|
||||
id: label,
|
||||
value: inputValue, // Always use the real value here
|
||||
onChange: this.handleInputChange,
|
||||
onFocus: this.handleFocus,
|
||||
onBlur: this.handleBlur,
|
||||
placeholder: placeholder,
|
||||
maxLength: maxLength,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(`wave-textfield wave-password ${className || ""}`, { focused: focused, error: error })}>
|
||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||
<div className="wave-textfield-inner">
|
||||
<label
|
||||
className={cn("wave-textfield-inner-label", {
|
||||
float: this.state.hasContent || this.state.focused || placeholder,
|
||||
"offset-left": decoration?.startDecoration,
|
||||
})}
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<If condition={passwordVisible}>
|
||||
<input {...inputProps} type="text" />
|
||||
</If>
|
||||
<If condition={!passwordVisible}>
|
||||
<input {...inputProps} type="password" />
|
||||
</If>
|
||||
<div
|
||||
className="wave-textfield-inner-eye"
|
||||
onClick={this.togglePasswordVisibility}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<If condition={passwordVisible}>
|
||||
<i className="fa-sharp fa-solid fa-eye"></i>
|
||||
</If>
|
||||
<If condition={!passwordVisible}>
|
||||
<i className="fa-sharp fa-solid fa-eye-slash"></i>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { PasswordField };
|
9
src/app/common/elements/resizablesidebar.less
Normal file
9
src/app/common/elements/resizablesidebar.less
Normal file
@ -0,0 +1,9 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.sidebar-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 5px;
|
||||
cursor: col-resize;
|
||||
}
|
171
src/app/common/elements/resizablesidebar.tsx
Normal file
171
src/app/common/elements/resizablesidebar.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||
import { MagicLayout } from "../../magiclayout";
|
||||
|
||||
import "./resizablesidebar.less";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
interface ResizableSidebarProps {
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
position: "left" | "right";
|
||||
enableSnap?: boolean;
|
||||
className?: string;
|
||||
children?: (toggleCollapsed: () => void) => React.ReactNode;
|
||||
toggleCollapse?: () => void;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class ResizableSidebar extends React.Component<ResizableSidebarProps> {
|
||||
resizeStartWidth: number = 0;
|
||||
startX: number = 0;
|
||||
prevDelta: number = 0;
|
||||
prevDragDirection: string = null;
|
||||
disposeReaction: any;
|
||||
|
||||
@boundMethod
|
||||
startResizing(event: React.MouseEvent<HTMLDivElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
const { parentRef, position } = this.props;
|
||||
const parentRect = parentRef.current?.getBoundingClientRect();
|
||||
|
||||
if (!parentRect) return;
|
||||
|
||||
if (position === "right") {
|
||||
this.startX = parentRect.right - event.clientX;
|
||||
} else {
|
||||
this.startX = event.clientX - parentRect.left;
|
||||
}
|
||||
|
||||
const mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
const collapsed = mainSidebarModel.getCollapsed();
|
||||
|
||||
this.resizeStartWidth = mainSidebarModel.getWidth();
|
||||
document.addEventListener("mousemove", this.onMouseMove);
|
||||
document.addEventListener("mouseup", this.stopResizing);
|
||||
|
||||
document.body.style.cursor = "col-resize";
|
||||
mobx.action(() => {
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(this.resizeStartWidth, collapsed);
|
||||
mainSidebarModel.isDragging.set(true);
|
||||
})();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onMouseMove(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
const { parentRef, enableSnap, position } = this.props;
|
||||
const parentRect = parentRef.current?.getBoundingClientRect();
|
||||
const mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
|
||||
if (!mainSidebarModel.isDragging.get() || !parentRect) return;
|
||||
|
||||
let delta: number, newWidth: number;
|
||||
|
||||
if (position === "right") {
|
||||
delta = parentRect.right - event.clientX - this.startX;
|
||||
} else {
|
||||
delta = event.clientX - parentRect.left - this.startX;
|
||||
}
|
||||
|
||||
newWidth = this.resizeStartWidth + delta;
|
||||
|
||||
if (enableSnap) {
|
||||
const minWidth = MagicLayout.MainSidebarMinWidth;
|
||||
const snapPoint = minWidth + MagicLayout.MainSidebarSnapThreshold;
|
||||
const dragResistance = MagicLayout.MainSidebarDragResistance;
|
||||
let dragDirection: string;
|
||||
|
||||
if (delta - this.prevDelta > 0) {
|
||||
dragDirection = "+";
|
||||
} else if (delta - this.prevDelta == 0) {
|
||||
if (this.prevDragDirection == "+") {
|
||||
dragDirection = "+";
|
||||
} else {
|
||||
dragDirection = "-";
|
||||
}
|
||||
} else {
|
||||
dragDirection = "-";
|
||||
}
|
||||
|
||||
this.prevDelta = delta;
|
||||
this.prevDragDirection = dragDirection;
|
||||
|
||||
if (newWidth - dragResistance > minWidth && newWidth < snapPoint && dragDirection == "+") {
|
||||
newWidth = snapPoint;
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
|
||||
} else if (newWidth + dragResistance < snapPoint && dragDirection == "-") {
|
||||
newWidth = minWidth;
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
|
||||
} else if (newWidth > snapPoint) {
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
|
||||
}
|
||||
} else {
|
||||
if (newWidth <= MagicLayout.MainSidebarMinWidth) {
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
|
||||
} else {
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
stopResizing() {
|
||||
let mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
|
||||
GlobalCommandRunner.clientSetSidebar(
|
||||
mainSidebarModel.tempWidth.get(),
|
||||
mainSidebarModel.tempCollapsed.get()
|
||||
).finally(() => {
|
||||
mobx.action(() => {
|
||||
mainSidebarModel.isDragging.set(false);
|
||||
})();
|
||||
});
|
||||
|
||||
document.removeEventListener("mousemove", this.onMouseMove);
|
||||
document.removeEventListener("mouseup", this.stopResizing);
|
||||
document.body.style.cursor = "";
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
toggleCollapsed() {
|
||||
const mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
|
||||
const tempCollapsed = mainSidebarModel.getCollapsed();
|
||||
const width = mainSidebarModel.getWidth(true);
|
||||
mainSidebarModel.setTempWidthAndTempCollapsed(width, !tempCollapsed);
|
||||
GlobalCommandRunner.clientSetSidebar(width, !tempCollapsed);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, children } = this.props;
|
||||
const mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
const width = mainSidebarModel.getWidth();
|
||||
const isCollapsed = mainSidebarModel.getCollapsed();
|
||||
|
||||
return (
|
||||
<div className={cn("sidebar", className, { collapsed: isCollapsed })} style={{ width }}>
|
||||
<div className="sidebar-content">{children(this.toggleCollapsed)}</div>
|
||||
<div
|
||||
className="sidebar-handle"
|
||||
style={{
|
||||
[this.props.position === "left" ? "right" : "left"]: 0,
|
||||
}}
|
||||
onMouseDown={this.startResizing}
|
||||
onDoubleClick={this.toggleCollapsed}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { ResizableSidebar };
|
36
src/app/common/elements/settingserror.tsx
Normal file
36
src/app/common/elements/settingserror.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
@mobxReact.observer
|
||||
class SettingsError extends React.Component<{ errorMessage: OV<string> }, {}> {
|
||||
@boundMethod
|
||||
dismissError(): void {
|
||||
mobx.action(() => {
|
||||
this.props.errorMessage.set(null);
|
||||
})();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.errorMessage.get() == null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="settings-field settings-error">
|
||||
<div>Error: {this.props.errorMessage.get()}</div>
|
||||
<div className="flex-spacer" />
|
||||
<div onClick={this.dismissError} className="error-dismiss">
|
||||
<i className="fa-sharp fa-solid fa-xmark" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { SettingsError };
|
28
src/app/common/elements/showwaveshellinstallprompt.tsx
Normal file
28
src/app/common/elements/showwaveshellinstallprompt.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { GlobalModel } from "../../../model/model";
|
||||
import * as appconst from "../../appconst";
|
||||
|
||||
function ShowWaveShellInstallPrompt(callbackFn: () => void) {
|
||||
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).
|
||||
`;
|
||||
message = message.trim();
|
||||
let prtn = GlobalModel.showAlert({
|
||||
message: message,
|
||||
confirm: true,
|
||||
markdown: true,
|
||||
confirmflag: appconst.ConfirmKey_HideShellPrompt,
|
||||
});
|
||||
prtn.then((confirm) => {
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
if (callbackFn) {
|
||||
callbackFn();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { ShowWaveShellInstallPrompt };
|
30
src/app/common/elements/status.less
Normal file
30
src/app/common/elements/status.less
Normal file
@ -0,0 +1,30 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.dot {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dot.green {
|
||||
background-color: @status-connected;
|
||||
}
|
||||
|
||||
.dot.red {
|
||||
background-color: @status-error;
|
||||
}
|
||||
|
||||
.dot.gray {
|
||||
background-color: @status-disconnected;
|
||||
}
|
||||
|
||||
.dot.yellow {
|
||||
background-color: @status-connecting;
|
||||
}
|
||||
}
|
34
src/app/common/elements/status.tsx
Normal file
34
src/app/common/elements/status.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
|
||||
import "./status.less";
|
||||
|
||||
interface StatusProps {
|
||||
status: "green" | "red" | "gray" | "yellow";
|
||||
text: string;
|
||||
}
|
||||
|
||||
class Status extends React.Component<StatusProps> {
|
||||
@boundMethod
|
||||
renderDot() {
|
||||
const { status } = this.props;
|
||||
|
||||
return <div className={`dot ${status}`} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { text } = this.props;
|
||||
|
||||
return (
|
||||
<div className="wave-status-container">
|
||||
{this.renderDot()}
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Status };
|
82
src/app/common/elements/textfield.less
Normal file
82
src/app/common/elements/textfield.less
Normal file
@ -0,0 +1,82 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-textfield {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
height: 44px;
|
||||
min-width: 412px;
|
||||
gap: 6px;
|
||||
border: 1px solid rgba(241, 246, 243, 0.15);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||
|
||||
&:hover {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
border-color: @term-green;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: @term-red;
|
||||
}
|
||||
|
||||
&-inner {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
|
||||
&-label {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 16px;
|
||||
font-size: 12.5px;
|
||||
transition: all 0.3s;
|
||||
color: @text-secondary;
|
||||
line-height: 10px;
|
||||
|
||||
&.float {
|
||||
font-size: 10px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&.offset-left {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
border: none;
|
||||
padding: 5px 0 5px 16px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
color: @term-bright-white;
|
||||
line-height: 20px;
|
||||
|
||||
&.offset-left {
|
||||
padding: 5px 16px 5px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.no-label {
|
||||
height: 34px;
|
||||
|
||||
input {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
173
src/app/common/elements/textfield.tsx
Normal file
173
src/app/common/elements/textfield.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
|
||||
import "./textfield.less";
|
||||
|
||||
interface TextFieldDecorationProps {
|
||||
startDecoration?: React.ReactNode;
|
||||
endDecoration?: React.ReactNode;
|
||||
}
|
||||
interface TextFieldProps {
|
||||
label?: string;
|
||||
value?: string;
|
||||
className?: string;
|
||||
onChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
decoration?: TextFieldDecorationProps;
|
||||
required?: boolean;
|
||||
maxLength?: number;
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface TextFieldState {
|
||||
focused: boolean;
|
||||
internalValue: string;
|
||||
error: boolean;
|
||||
showHelpText: boolean;
|
||||
hasContent: boolean;
|
||||
}
|
||||
|
||||
class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
state: TextFieldState;
|
||||
|
||||
constructor(props: TextFieldProps) {
|
||||
super(props);
|
||||
const hasInitialContent = Boolean(props.value || props.defaultValue);
|
||||
this.state = {
|
||||
focused: false,
|
||||
hasContent: hasInitialContent,
|
||||
internalValue: props.defaultValue || "",
|
||||
error: false,
|
||||
showHelpText: false,
|
||||
};
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: TextFieldProps) {
|
||||
// Only update the focus state if using as controlled
|
||||
if (this.props.value !== undefined && this.props.value !== prevProps.value) {
|
||||
this.setState({ focused: Boolean(this.props.value) });
|
||||
}
|
||||
}
|
||||
|
||||
// Method to handle focus at the component level
|
||||
@boundMethod
|
||||
handleComponentFocus() {
|
||||
if (this.inputRef.current && !this.inputRef.current.contains(document.activeElement)) {
|
||||
this.inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Method to handle blur at the component level
|
||||
@boundMethod
|
||||
handleComponentBlur() {
|
||||
if (this.inputRef.current?.contains(document.activeElement)) {
|
||||
this.inputRef.current.blur();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleFocus() {
|
||||
this.setState({ focused: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleBlur() {
|
||||
const { required } = this.props;
|
||||
if (this.inputRef.current) {
|
||||
const value = this.inputRef.current.value;
|
||||
if (required && !value) {
|
||||
this.setState({ error: true, focused: false });
|
||||
} else {
|
||||
this.setState({ error: false, focused: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleHelpTextClick() {
|
||||
this.setState((prevState) => ({ showHelpText: !prevState.showHelpText }));
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { required, onChange } = this.props;
|
||||
const inputValue = e.target.value;
|
||||
|
||||
// Check if value is empty and the field is required
|
||||
if (required && !inputValue) {
|
||||
this.setState({ error: true, hasContent: false });
|
||||
} else {
|
||||
this.setState({ error: false, hasContent: Boolean(inputValue) });
|
||||
}
|
||||
|
||||
// Update the internal state for uncontrolled version
|
||||
if (this.props.value === undefined) {
|
||||
this.setState({ internalValue: inputValue });
|
||||
}
|
||||
|
||||
onChange && onChange(inputValue);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, value, placeholder, decoration, className, maxLength, autoFocus, disabled } = this.props;
|
||||
const { focused, internalValue, error } = this.state;
|
||||
|
||||
// Decide if the input should behave as controlled or uncontrolled
|
||||
const inputValue = value ?? internalValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("wave-textfield", className, {
|
||||
focused: focused,
|
||||
error: error,
|
||||
disabled: disabled,
|
||||
"no-label": !label,
|
||||
})}
|
||||
onFocus={this.handleComponentFocus}
|
||||
onBlur={this.handleComponentBlur}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||
<div className="wave-textfield-inner">
|
||||
<If condition={label}>
|
||||
<label
|
||||
className={cn("wave-textfield-inner-label", {
|
||||
float: this.state.hasContent || this.state.focused || placeholder,
|
||||
"offset-left": decoration?.startDecoration,
|
||||
})}
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
</If>
|
||||
<input
|
||||
className={cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration })}
|
||||
ref={this.inputRef}
|
||||
id={label}
|
||||
value={inputValue}
|
||||
onChange={this.handleInputChange}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { TextField };
|
||||
export type { TextFieldProps, TextFieldDecorationProps, TextFieldState };
|
47
src/app/common/elements/toggle.less
Normal file
47
src/app/common/elements/toggle.less
Normal file
@ -0,0 +1,47 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.checkbox-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
content: "";
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #333;
|
||||
transition: 0.5s;
|
||||
border-radius: 33px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: @term-white;
|
||||
transition: 0.5s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: @term-green;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
}
|
28
src/app/common/elements/toggle.tsx
Normal file
28
src/app/common/elements/toggle.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
|
||||
import "./toggle.less";
|
||||
|
||||
class Toggle extends React.Component<{ checked: boolean; onChange: (value: boolean) => void }, {}> {
|
||||
@boundMethod
|
||||
handleChange(e: any): void {
|
||||
let { onChange } = this.props;
|
||||
if (onChange != null) {
|
||||
onChange(e.target.checked);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<label className="checkbox-toggle">
|
||||
<input type="checkbox" checked={this.props.checked} onChange={this.handleChange} />
|
||||
<span className="slider" />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Toggle };
|
23
src/app/common/elements/tooltip.less
Normal file
23
src/app/common/elements/tooltip.less
Normal file
@ -0,0 +1,23 @@
|
||||
@import "../../../app/common/themes/themes.less";
|
||||
|
||||
.wave-tooltip {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #777;
|
||||
background-color: #444;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
width: 300px;
|
||||
|
||||
i {
|
||||
display: inline;
|
||||
font-size: 13px;
|
||||
fill: @base-color;
|
||||
padding-top: 0.2em;
|
||||
}
|
||||
}
|
84
src/app/common/elements/tooltip.tsx
Normal file
84
src/app/common/elements/tooltip.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import "./tooltip.less";
|
||||
|
||||
interface TooltipProps {
|
||||
message: React.ReactNode;
|
||||
icon?: React.ReactNode; // Optional icon property
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface TooltipState {
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class Tooltip extends React.Component<TooltipProps, TooltipState> {
|
||||
iconRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: TooltipProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isVisible: false,
|
||||
};
|
||||
this.iconRef = React.createRef();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
showBubble() {
|
||||
this.setState({ isVisible: true });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
hideBubble() {
|
||||
this.setState({ isVisible: false });
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
calculatePosition() {
|
||||
// Get the position of the icon element
|
||||
const iconElement = this.iconRef.current;
|
||||
if (iconElement) {
|
||||
const rect = iconElement.getBoundingClientRect();
|
||||
return {
|
||||
top: `${rect.bottom + window.scrollY - 29}px`,
|
||||
left: `${rect.left + window.scrollX + rect.width / 2 - 17.5}px`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
renderBubble() {
|
||||
if (!this.state.isVisible) return null;
|
||||
|
||||
const style = this.calculatePosition();
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div className={cn("wave-tooltip", this.props.className)} style={style}>
|
||||
{this.props.icon && <div className="wave-tooltip-icon">{this.props.icon}</div>}
|
||||
<div className="wave-tooltip-message">{this.props.message}</div>
|
||||
</div>,
|
||||
document.getElementById("app")!
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div onMouseEnter={this.showBubble} onMouseLeave={this.hideBubble} ref={this.iconRef}>
|
||||
{this.props.children}
|
||||
{this.renderBubble()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Tooltip };
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel } from "../../../model/model";
|
||||
import { Modal, LinkButton } from "../common";
|
||||
import { Modal, LinkButton } from "../elements";
|
||||
import * as util from "../../../util/util";
|
||||
|
||||
import logo from "../../assets/waveterm-logo-with-bg.svg";
|
||||
|
@ -5,7 +5,7 @@ import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { Markdown, Modal, Button, Checkbox } from "../common";
|
||||
import { Markdown, Modal, Button, Checkbox } from "../elements";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||
|
||||
import "./alert.less";
|
||||
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { GlobalModel } from "../../../model/model";
|
||||
import { Modal, Button } from "../common";
|
||||
import { Modal, Button } from "../elements";
|
||||
|
||||
import "./clientstop.less";
|
||||
|
||||
|
@ -8,9 +8,17 @@ import { boundMethod } from "autobind-decorator";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||
import * as T from "../../../types/types";
|
||||
import { Modal, TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip, ShowWaveShellInstallPrompt } from "../common";
|
||||
import {
|
||||
Modal,
|
||||
TextField,
|
||||
NumberField,
|
||||
InputDecoration,
|
||||
Dropdown,
|
||||
PasswordField,
|
||||
Tooltip,
|
||||
ShowWaveShellInstallPrompt,
|
||||
} from "../elements";
|
||||
import * as util from "../../../util/util";
|
||||
import * as appconst from "../../appconst";
|
||||
|
||||
import "./createremoteconn.less";
|
||||
|
||||
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel } from "../../../model/model";
|
||||
import { Modal, Button } from "../common";
|
||||
import { Modal, Button } from "../elements";
|
||||
|
||||
import "./disconnected.less";
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { If } from "tsx-control-statements/components";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||
import * as T from "../../../types/types";
|
||||
import { Modal, TextField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common";
|
||||
import { Modal, TextField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../elements";
|
||||
import * as util from "../../../util/util";
|
||||
|
||||
import "./editremoteconn.less";
|
||||
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||
import { SettingsError, Modal, Dropdown } from "../common";
|
||||
import { SettingsError, Modal, Dropdown } from "../elements";
|
||||
import { LineType, RendererPluginType } from "../../../types/types";
|
||||
import { PluginModel } from "../../../plugins/plugins";
|
||||
import { commandRtnHandler } from "../../../util/util";
|
||||
|
@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner, TabColors, TabIcons, Screen } from "../../../model/model";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Dropdown, Tooltip } from "../common";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Dropdown, Tooltip } from "../elements";
|
||||
import { RemoteType } from "../../../types/types";
|
||||
import * as util from "../../../util/util";
|
||||
import { commandRtnHandler } from "../../../util/util";
|
||||
|
@ -14,6 +14,15 @@
|
||||
gap: 4px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
|
||||
.settings-label > div:first-child {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-settings-tooltip i {
|
||||
font-size: 12px;
|
||||
margin-left: 0.5px;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel, GlobalCommandRunner, Session } from "../../../model/model";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage, Modal } from "../common";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Tooltip } from "../elements";
|
||||
import * as util from "../../../util/util";
|
||||
import { commandRtnHandler } from "../../../util/util";
|
||||
|
||||
@ -111,10 +111,14 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">
|
||||
<div>Archived</div>
|
||||
<InfoMessage width={400}>
|
||||
Archive will hide the workspace from the active menu. Commands and output will be
|
||||
retained in history.
|
||||
</InfoMessage>
|
||||
<Tooltip
|
||||
className="session-settings-tooltip"
|
||||
message="Archive will hide the workspace from the active menu. Commands and output will be
|
||||
retained in history."
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
{<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="settings-input">
|
||||
<Toggle checked={this.session.archived.get()} onChange={this.handleChangeArchived} />
|
||||
@ -123,9 +127,13 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
||||
<div className="settings-field">
|
||||
<div className="settings-label">
|
||||
<div>Actions</div>
|
||||
<InfoMessage width={400}>
|
||||
Delete will remove the workspace, removing all commands and output from history.
|
||||
</InfoMessage>
|
||||
<Tooltip
|
||||
className="session-settings-tooltip"
|
||||
message="Delete will remove the workspace, removing all commands and output from history."
|
||||
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
>
|
||||
{<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="settings-input">
|
||||
<div
|
||||
|
@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator";
|
||||
import { For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||
import { Modal, TextField, InputDecoration, Tooltip } from "../common";
|
||||
import { Modal, TextField, InputDecoration, Tooltip } from "../elements";
|
||||
import * as util from "../../../util/util";
|
||||
import { Screen } from "../../../model/model";
|
||||
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||
|
@ -5,7 +5,7 @@ import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||
import { Toggle, Modal, Button } from "../common";
|
||||
import { Toggle, Modal, Button } from "../elements";
|
||||
import * as util from "../../../util/util";
|
||||
import { ClientDataType } from "../../../types/types";
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { If, For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||
import * as T from "../../../types/types";
|
||||
import { Modal, Tooltip, Button, Status } from "../common";
|
||||
import { Modal, Tooltip, Button, Status } from "../elements";
|
||||
import * as util from "../../../util/util";
|
||||
import * as textmeasure from "../../../util/textmeasure";
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../model/model";
|
||||
import { Button, IconButton, Status, ShowWaveShellInstallPrompt } from "../common/common";
|
||||
import { Button, Status, ShowWaveShellInstallPrompt } from "../common/elements";
|
||||
import * as T from "../../types/types";
|
||||
import * as util from "../../util/util";
|
||||
import * as appconst from "../appconst";
|
||||
|
@ -14,7 +14,7 @@ import dayjs from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import { Line } from "../line/linecomps";
|
||||
import { CmdStrCode } from "../common/common";
|
||||
import { CmdStrCode } from "../common/elements";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
||||
|
||||
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
|
||||
|
@ -23,7 +23,7 @@ import type {
|
||||
import cn from "classnames";
|
||||
|
||||
import type { LineContainerModel } from "../../model/model";
|
||||
import { renderCmdText } from "../common/common";
|
||||
import { renderCmdText } from "../common/elements";
|
||||
import { SimpleBlobRenderer } from "../../plugins/core/basicrenderer";
|
||||
import { IncrementalRenderer } from "../../plugins/core/incrementalrenderer";
|
||||
import { TerminalRenderer } from "../../plugins/terminal/terminal";
|
||||
|
@ -7,7 +7,7 @@ import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel } from "../../model/model";
|
||||
import { PluginModel } from "../../plugins/plugins";
|
||||
import { Markdown } from "../common/common";
|
||||
import { Markdown } from "../common/elements";
|
||||
|
||||
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
|
||||
|
||||
@ -22,7 +22,7 @@ class PluginsView extends React.Component<{}, {}> {
|
||||
|
||||
renderPluginIcon(plugin): any {
|
||||
let Comp = plugin.iconComp;
|
||||
return <Comp/>;
|
||||
return <Comp />;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -19,7 +19,7 @@ import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
|
||||
import { isBlank, openLink } from "../../util/util";
|
||||
import { ResizableSidebar } from "../common/common";
|
||||
import { ResizableSidebar } from "../common/elements";
|
||||
import * as constants from "../appconst";
|
||||
|
||||
import "./sidebar.less";
|
||||
|
@ -12,7 +12,7 @@ import { Prompt } from "../../common/prompt/prompt";
|
||||
import { TextAreaInput } from "./textareainput";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import type { OpenAICmdInfoChatMessageType } from "../../../types/types";
|
||||
import { Markdown } from "../../common/common";
|
||||
import { Markdown } from "../../common/elements";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../../util/keyutil";
|
||||
|
||||
@mobxReact.observer
|
||||
|
@ -11,7 +11,7 @@ import dayjs from "dayjs";
|
||||
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
||||
import { renderCmdText } from "../../common/common";
|
||||
import { renderCmdText } from "../../common/elements";
|
||||
import { TextAreaInput } from "./textareainput";
|
||||
import { InfoMsg } from "./infomsg";
|
||||
import { HistoryInfo } from "./historyinfo";
|
||||
|
@ -10,16 +10,24 @@ import { If, For } from "tsx-control-statements/components";
|
||||
import cn from "classnames";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import dayjs from "dayjs";
|
||||
import { GlobalCommandRunner, TabColors, TabIcons, ForwardLineContainer, GlobalModel, ScreenLines, Screen, Session } from "../../../model/model";
|
||||
import {
|
||||
GlobalCommandRunner,
|
||||
TabColors,
|
||||
TabIcons,
|
||||
ForwardLineContainer,
|
||||
GlobalModel,
|
||||
ScreenLines,
|
||||
Screen,
|
||||
Session,
|
||||
} from "../../../model/model";
|
||||
import type { LineType, RenderModeType, LineFactoryProps } from "../../../types/types";
|
||||
import * as T from "../../../types/types";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { Button } from "../../common/common";
|
||||
import { Button, TextField, Dropdown } from "../../common/elements";
|
||||
import { getRemoteStr } from "../../common/prompt/prompt";
|
||||
import { Line } from "../../line/linecomps";
|
||||
import { LinesView } from "../../line/linesview";
|
||||
import * as util from "../../../util/util";
|
||||
import { TextField, Dropdown } from "../../common/common";
|
||||
import { ReactComponent as EllipseIcon } from "../../assets/icons/ellipse.svg";
|
||||
import { ReactComponent as Check12Icon } from "../../assets/icons/check12.svg";
|
||||
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||
|
@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
||||
import { ActionsIcon, StatusIndicator, CenteredIcon } from "../../common/icons/icons";
|
||||
import { renderCmdText } from "../../common/common";
|
||||
import { renderCmdText } from "../../common/elements";
|
||||
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||
import * as constants from "../../appconst";
|
||||
import { Reorder } from "framer-motion";
|
||||
|
@ -5,7 +5,7 @@ import * as React from "react";
|
||||
import * as T from "../../types/types";
|
||||
import Editor, { Monaco } from "@monaco-editor/react";
|
||||
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import { Markdown } from "../../app/common/common";
|
||||
import { Markdown } from "../../app/common/elements";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
|
||||
import Split from "react-split-it";
|
||||
import loader from "@monaco-editor/loader";
|
||||
|
@ -6,7 +6,7 @@ import * as mobx from "mobx";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as T from "../../types/types";
|
||||
import { sprintf } from "sprintf-js";
|
||||
import { Markdown } from "../../app/common/common";
|
||||
import { Markdown } from "../../app/common/elements";
|
||||
|
||||
import "./markdown.less";
|
||||
|
||||
@ -17,7 +17,13 @@ const DefaultMaxMarkdownWidth = 1000;
|
||||
|
||||
@mobxReact.observer
|
||||
class SimpleMarkdownRenderer extends React.Component<
|
||||
{ data: T.ExtBlob; context: T.RendererContext; opts: T.RendererOpts; savedHeight: number, lineState: T.LineStateType },
|
||||
{
|
||||
data: T.ExtBlob;
|
||||
context: T.RendererContext;
|
||||
opts: T.RendererOpts;
|
||||
savedHeight: number;
|
||||
lineState: T.LineStateType;
|
||||
},
|
||||
{}
|
||||
> {
|
||||
markdownText: OV<string> = mobx.observable.box(null, { name: "markdownText" });
|
||||
@ -74,7 +80,10 @@ class SimpleMarkdownRenderer extends React.Component<
|
||||
maxHeight: opts.maxSize.height,
|
||||
}}
|
||||
>
|
||||
<Markdown text={this.markdownText.get()} style={{ maxHeight: opts.maxSize.height, maxWidth: DefaultMaxMarkdownWidth }} />
|
||||
<Markdown
|
||||
text={this.markdownText.get()}
|
||||
style={{ maxHeight: opts.maxSize.height, maxWidth: DefaultMaxMarkdownWidth }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ import * as T from "../../types/types";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { PacketDataBuffer } from "../core/ptydata";
|
||||
import { Markdown } from "../../app/common/common";
|
||||
import { Markdown } from "../../app/common/elements";
|
||||
|
||||
import "./openai.less";
|
||||
|
||||
@ -207,7 +207,7 @@ class OpenAIRenderer extends React.Component<{ model: OpenAIRendererModel }> {
|
||||
<div
|
||||
style={{
|
||||
maxHeight: opts.maxSize.height,
|
||||
paddingRight: 5
|
||||
paddingRight: 5,
|
||||
}}
|
||||
>
|
||||
<Markdown text={message} style={{ maxHeight: opts.maxSize.height }} />
|
||||
@ -236,18 +236,18 @@ class OpenAIRenderer extends React.Component<{ model: OpenAIRendererModel }> {
|
||||
let cmd = model.rawCmd;
|
||||
let styleVal: Record<string, any> = null;
|
||||
if (model.loading.get() && model.savedHeight >= 0 && model.isDone) {
|
||||
styleVal = {
|
||||
styleVal = {
|
||||
height: model.savedHeight,
|
||||
maxHeight: model.opts.maxSize.height
|
||||
maxHeight: model.opts.maxSize.height,
|
||||
};
|
||||
} else {
|
||||
let maxWidth = model.opts.maxSize.width
|
||||
if(maxWidth > 1000) {
|
||||
maxWidth = 1000
|
||||
let maxWidth = model.opts.maxSize.width;
|
||||
if (maxWidth > 1000) {
|
||||
maxWidth = 1000;
|
||||
}
|
||||
styleVal = {
|
||||
styleVal = {
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: model.opts.maxSize.height
|
||||
maxHeight: model.opts.maxSize.height,
|
||||
};
|
||||
}
|
||||
let version = model.version.get();
|
||||
|
@ -11,6 +11,9 @@ type RemoteStatusTypeStrs = "connected" | "connecting" | "disconnected" | "error
|
||||
type LineContainerStrs = "main" | "sidebar" | "history";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
type OArr<V> = mobx.IObservableArray<V>;
|
||||
type OMap<K, V> = mobx.ObservableMap<K, V>;
|
||||
type CV<V> = mobx.IComputedValue<V>;
|
||||
|
||||
type SessionDataType = {
|
||||
sessionid: string;
|
||||
@ -811,6 +814,7 @@ export type {
|
||||
CmdInputTextPacketType,
|
||||
OpenAICmdInfoChatMessageType,
|
||||
ScreenStatusIndicatorUpdateType,
|
||||
OV,
|
||||
};
|
||||
|
||||
export { StatusIndicatorLevel };
|
||||
|
Loading…
Reference in New Issue
Block a user