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:
Red J Adaya 2024-02-07 05:23:56 +08:00 committed by GitHub
parent fc65e65f11
commit bf1b556537
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 2560 additions and 2652 deletions

View File

@ -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";

View File

@ -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

View 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;
}
}

View 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 };

View 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;
}
}
}

View 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 };

View 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;
}

View 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 };

View 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>&#x2318;{text}</span>;
}
export { renderCmdText };

View 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;
}

View 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 };

View 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 };

View 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";

View 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;
}
}
}

View 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 };

View 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;
}

View 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 };

View 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 };

View 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;
}

View 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 };

View 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;
}
}
}
}

View 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 };

View 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 };

View 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;
}
}

View 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 };

View File

@ -0,0 +1,9 @@
@import "../../../app/common/themes/themes.less";
.sidebar-handle {
position: absolute;
top: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
}

View 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 };

View 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 };

View 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 };

View 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;
}
}

View 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 };

View 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;
}
}
}

View 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 };

View 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);
}
}

View 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 };

View 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;
}
}

View 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 };

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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;
}

View File

@ -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

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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() {

View File

@ -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";

View File

@ -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

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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>
);

View File

@ -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();

View File

@ -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 };