about modal (#244)

This commit is contained in:
Red J Adaya 2024-08-20 06:49:40 +08:00 committed by GitHub
parent 534fcc5d0a
commit 964c422a02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 353 additions and 185 deletions

View File

@ -690,7 +690,10 @@ function getAppMenu() {
]; ];
const appMenu: Electron.MenuItemConstructorOptions[] = [ const appMenu: Electron.MenuItemConstructorOptions[] = [
{ {
role: "about", label: "About Wave Terminal",
click: (_, window) => {
window?.webContents.send("menu-item-about");
},
}, },
{ {
label: "Check for Updates", label: "Check for Updates",

View File

@ -24,6 +24,7 @@ contextBridge.exposeInMainWorld("api", {
onUpdaterStatusChange: (callback) => ipcRenderer.on("app-update-status", (_event, status) => callback(status)), onUpdaterStatusChange: (callback) => ipcRenderer.on("app-update-status", (_event, status) => callback(status)),
getUpdaterStatus: () => ipcRenderer.sendSync("get-app-update-status"), getUpdaterStatus: () => ipcRenderer.sendSync("get-app-update-status"),
installAppUpdate: () => ipcRenderer.send("install-app-update"), installAppUpdate: () => ipcRenderer.send("install-app-update"),
onMenuItemAbout: (callback) => ipcRenderer.on("menu-item-about", callback),
updateWindowControlsOverlay: (rect) => ipcRenderer.send("update-window-controls-overlay", rect), updateWindowControlsOverlay: (rect) => ipcRenderer.send("update-window-controls-overlay", rect),
}); });

View File

@ -2,150 +2,102 @@
/* SPDX-License-Identifier: Apache-2.0 */ /* SPDX-License-Identifier: Apache-2.0 */
.button { .button {
background: var(--accent-color); padding: 1px;
border: none; border: 1px solid transparent;
cursor: pointer; border-radius: 8px;
outline: inherit;
display: flex;
padding: 6px 16px;
align-items: center;
gap: 4px;
border-radius: 6px;
height: auto;
line-height: 1.5;
white-space: nowrap;
user-select: none;
font-weight: 500;
color: var(--main-text-color); &:focus,
&.focus {
i { border: 1px solid var(--button-focus-border-color);
fill: var(--main-text-color);
} }
&.primary, .button-inner {
&.secondary {
color: var(--main-text-color);
background: var(--accent-color); background: var(--accent-color);
border: none;
cursor: pointer;
outline: inherit;
display: flex;
padding: 8px 20px;
align-items: center;
gap: 4px;
border-radius: 6px;
height: auto;
line-height: 16px;
white-space: nowrap;
user-select: none;
font-size: 14px;
color: var(--main-text-color);
i { i {
fill: var(--main-text-color); fill: var(--main-text-color);
} }
}
&.primary.danger { &.primary,
background: var(--error-color); &.secondary {
}
&.primary.warning {
background: #e6ba1e;
color: #000000;
}
&.primary.outlined,
&.primary.greyoutlined {
background: none;
border: 1px solid var(--accent-color);
i {
fill: var(--accent-color);
}
}
&.primary.greyoutlined {
border-color: var(--secondary-text-color);
i {
fill: var(--secondary-text-color);
}
}
&.primary.outlined.danger {
border-color: var(--error-color);
i {
fill: var(--error-color);
}
}
&.primary.outlined,
&.primary.greyoutlined {
&.hover-danger:hover {
color: var(--main-text-color); color: var(--main-text-color);
border-color: var(--error-color); background: var(--accent-color);
i {
fill: var(--main-text-color);
}
}
&.primary.danger {
background: var(--error-color); background: var(--error-color);
} }
}
&.greytext { &.primary.warning {
color: var(--secondary-text-color); background: #e6ba1e;
} color: #000000;
}
&.primary.ghost { &.primary.ghost {
background: none; background: none;
i { i {
fill: var(--accent-color); fill: var(--accent-color);
}
}
&.primary.ghost.danger {
i {
fill: var(--app-error-color);
}
}
&.secondary,
&.link-button {
background: var(--highlight-bg-color);
}
&.secondary.ghost {
background: none;
}
&.secondary.danger {
color: var(--error-color);
}
&.disabled {
opacity: 0.5;
} }
} }
&.primary.ghost.danger { /*
i { * customs styles here
fill: var(--app-error-color); */
} .border-radius-4 {
border-radius: 4px;
} }
&.secondary { .vertical-padding-2 {
background: var(--highlight-bg-color); padding-top: 2px;
padding-bottom: 2px;
} }
&.secondary.outlined { .horizontal-padding-10 {
background: none; padding-left: 10px;
border: 1px solid var(--main-text-color); padding-right: 10px;
}
&.secondary.outlined.danger {
border-color: var(--error-color);
}
&.secondary.ghost {
background: none;
}
&.secondary.danger {
color: var(--error-color);
}
&.small {
padding: 4px 8px;
font-size: 12px;
border-radius: 3.6px;
}
&.term-inline {
padding: 2px 8px;
border-radius: 3px;
}
&.disabled {
opacity: 0.5;
}
&.link {
cursor: pointer;
} }
} }
.border-radius-4 {
border-radius: 4px;
}
.vertical-padding-2 {
padding-top: 2px;
padding-bottom: 2px;
}
.horizontal-padding-10 {
padding-left: 10px;
padding-right: 10px;
}

View File

@ -14,16 +14,18 @@ const Button = React.memo(({ className = "primary", children, disabled, ...props
); );
return ( return (
<button <div className="button">
className={clsx("button", className, { <button
disabled, className={clsx("button-inner", className, {
hasIcon, disabled,
})} hasIcon,
disabled={disabled} })}
{...props} disabled={disabled}
> {...props}
{children} >
</button> {children}
</button>
</div>
); );
}); });

View File

@ -0,0 +1,12 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.button {
&.link-button {
text-decoration: none;
.button-inner {
padding: 8px 12px;
}
}
}

View File

@ -0,0 +1,37 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { clsx } from "clsx";
import * as React from "react";
import "./linkbutton.less";
interface LinkButtonProps {
href: string;
rel?: string;
target?: string;
children: React.ReactNode;
disabled?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
style?: React.CSSProperties;
autoFocus?: boolean;
className?: string;
termInline?: boolean;
title?: string;
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
}
const LinkButton = ({ leftIcon, rightIcon, children, className, ...rest }: LinkButtonProps) => {
return (
<a {...rest} className="button link-button">
<span className={clsx("button-inner", className)}>
{leftIcon && <span className="icon-left">{leftIcon}</span>}
{children}
{rightIcon && <span className="icon-right">{rightIcon}</span>}
</span>
</a>
);
};
export { LinkButton };

View File

@ -0,0 +1,61 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.about-modal {
width: 445px;
padding-bottom: 34px;
.modal-content {
margin-bottom: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
.section-wrapper {
display: flex;
flex-direction: column;
gap: 26px;
width: 100%;
}
.section {
align-items: center;
gap: 16px;
align-self: stretch;
width: 100%;
text-align: center;
&.logo-section {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
.app-name {
font-size: 25px;
}
.text-standard {
line-height: 20px;
}
}
}
.section:nth-child(3) {
display: flex;
align-items: flex-start;
gap: 7px;
.wave-button-link {
display: flex;
align-items: center;
i {
font-size: 16px;
}
}
}
}
}

View File

@ -0,0 +1,61 @@
import Logo from "@/app/asset/logo.svg";
import { LinkButton } from "@/app/element/linkbutton";
import { modalsModel } from "@/app/store/modalmodel";
import { Modal } from "./modal";
import "./about.less";
interface AboutModalProps {}
const AboutModal = ({}: AboutModalProps) => {
const currentDate = new Date();
return (
<Modal className="about-modal" title="About" onClose={() => modalsModel.popModal()}>
<div className="section-wrapper">
<div className="section logo-section">
<Logo />
<div className="app-name">Wave Terminal</div>
<div className="text-standard">
Open-Source AI-Native Terminal
<br />
Built for Seamless Workflows
</div>
</div>
<div className="section text-standard">Client Version 0.1.8 (20240615-002636)</div>
<div className="section">
<LinkButton
className="secondary solid"
href="https://github.com/wavetermdev/waveterm"
target="_blank"
leftIcon={<i className="fa-brands fa-github"></i>}
>
Github
</LinkButton>
<LinkButton
className="secondary solid"
href="https://www.waveterm.dev/"
target="_blank"
leftIcon={<i className="fa-sharp fa-light fa-globe"></i>}
>
Website
</LinkButton>
<LinkButton
className="secondary solid"
href="https://github.com/wavetermdev/waveterm/blob/main/acknowledgements/README.md"
target="_blank"
rel={"noopener"}
leftIcon={<i className="fa-sharp fa-light fa-heart"></i>}
>
Acknowledgements
</LinkButton>
</div>
<div className="section text-standard">&copy; {currentDate.getFullYear()} Command Line Inc.</div>
</div>
</Modal>
);
};
AboutModal.displayName = "AboutModal";
export { AboutModal };

View File

@ -24,51 +24,61 @@
} }
.modal { .modal {
position: relative;
z-index: var(--zindex-modal); z-index: var(--zindex-modal);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 16px; gap: 32px;
border-radius: 10px; padding: 24px 16px 16px;
border-radius: 8px;
border: 0.5px solid var(--modal-border-color);
background: var(--modal-bg-color); background: var(--modal-bg-color);
border: 1px solid var(--border-color); box-shadow: 0px 8px 32px 0px rgba(0, 0, 0, 0.25);
box-shadow: 0px 5px 5px 5px rgba(0, 0, 0, 0.1);
.modal-header { .header-content-wrapper {
width: 100%;
display: flex; display: flex;
align-items: center; flex-direction: column;
padding: 12px 14px 12px 20px; gap: 8px;
justify-content: space-between; width: 100%;
line-height: 20px;
border-bottom: 1px solid var(--modal-header-bottom-border-color);
user-select: none;
.modal-title { .modal-header {
color: var(--main-text-color); width: 100%;
font-size: var(--title-font-size); display: flex;
} align-items: center;
justify-content: space-between;
user-select: none;
button { .modal-title {
padding-right: 4px !important; color: var(--main-text-color);
i { font-size: var(--title-font-size);
font-size: 18px; }
button {
position: absolute;
right: 8px;
top: 8px;
padding: 8px 16px;
i {
font-size: 18px;
}
} }
} }
} .modal-content {
width: 100%;
.modal-body { padding: 0px 20px;
width: 100%; }
padding: 0px 20px;
} }
.modal-footer { .modal-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
padding: 0 20px 20px; padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
button:last-child { .button:last-child {
margin-left: 8px; margin-left: 8px;
} }
} }

View File

@ -2,19 +2,18 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Button } from "@/app/element/button"; import { Button } from "@/app/element/button";
import clsx from "clsx";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import "./modal.less"; import "./modal.less";
interface ModalHeaderProps { interface ModalHeaderProps {
title: React.ReactNode;
description?: string; description?: string;
onClose?: () => void; onClose?: () => void;
} }
const ModalHeader = ({ onClose, title, description }: ModalHeaderProps) => ( const ModalHeader = ({ onClose, description }: ModalHeaderProps) => (
<header className="modal-header"> <header className="modal-header">
{typeof title === "string" ? <h3 className="modal-title">{title}</h3> : title}
{description && <p>{description}</p>} {description && <p>{description}</p>}
{onClose && ( {onClose && (
<Button className="secondary ghost" onClick={onClose} title="Close (ESC)"> <Button className="secondary ghost" onClick={onClose} title="Close (ESC)">
@ -39,20 +38,22 @@ interface ModalFooterProps {
onCancel?: () => void; onCancel?: () => void;
} }
const ModalFooter = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }: ModalFooterProps) => ( const ModalFooter = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }: ModalFooterProps) => {
<footer className="modal-footer"> return (
{onCancel && ( <footer className="modal-footer">
<Button className="secondary" onClick={onCancel}> {onCancel && (
{cancelLabel} <Button className="secondary ghost" onClick={onCancel}>
</Button> {cancelLabel}
)} </Button>
{onOk && ( )}
<Button className="primary" onClick={onOk}> {onOk && (
{okLabel} <Button className="primary" onClick={onOk}>
</Button> {okLabel}
)} </Button>
</footer> )}
); </footer>
);
};
interface ModalProps { interface ModalProps {
title: string; title: string;
@ -81,13 +82,21 @@ const Modal = ({
}: ModalProps) => { }: ModalProps) => {
const renderBackdrop = (onClick) => <div className="modal-backdrop" onClick={onClick}></div>; const renderBackdrop = (onClick) => <div className="modal-backdrop" onClick={onClick}></div>;
const renderFooter = () => {
return onOk || onCancel;
};
const renderModal = () => ( const renderModal = () => (
<div className="modal-wrapper"> <div className="modal-wrapper">
{renderBackdrop(onClickBackdrop)} {renderBackdrop(onClickBackdrop)}
<div className={`modal ${className}`}> <div className={clsx(`modal`, className)}>
<ModalHeader title={title} onClose={onClose} description={description} /> <div className="header-content-wrapper">
<ModalContent>{children}</ModalContent> <ModalHeader onClose={onClose} description={description} />
<ModalFooter onCancel={onCancel} onOk={onOk} cancelLabel={cancelLabel} okLabel={okLabel} /> <ModalContent>{children}</ModalContent>
</div>
{renderFooter() && (
<ModalFooter onCancel={onCancel} onOk={onOk} cancelLabel={cancelLabel} okLabel={okLabel} />
)}
</div> </div>
</div> </div>
); );

View File

@ -1,12 +1,14 @@
// Copyright 2023, Command Line Inc. // Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { AboutModal } from "./about";
import { TosModal } from "./tos"; import { TosModal } from "./tos";
import { UserInputModal } from "./userinputmodal"; import { UserInputModal } from "./userinputmodal";
const modalRegistry: { [key: string]: React.ComponentType<any> } = { const modalRegistry: { [key: string]: React.ComponentType<any> } = {
[TosModal.displayName || "TosModal"]: TosModal, [TosModal.displayName || "TosModal"]: TosModal,
[UserInputModal.displayName || "UserInputModal"]: UserInputModal, [UserInputModal.displayName || "UserInputModal"]: UserInputModal,
[AboutModal.displayName || "AboutModal"]: AboutModal,
}; };
export const getModalComponent = (key: string): React.ComponentType<any> | undefined => { export const getModalComponent = (key: string): React.ComponentType<any> | undefined => {

View File

@ -1,5 +1,6 @@
.tos-modal { .tos-modal {
width: 640px; width: 640px;
border-radius: 10px;
.modal-inner { .modal-inner {
padding: 40px 76px; padding: 40px 76px;
@ -16,6 +17,8 @@
.logo { .logo {
margin-bottom: 10px; margin-bottom: 10px;
display: flex;
justify-content: center;
} }
.modal-title { .modal-title {

View File

@ -62,6 +62,15 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
// do nothing // do nothing
} }
const showAboutModalAtom = jotai.atom(false) as jotai.PrimitiveAtom<boolean>;
try {
getApi().onMenuItemAbout(() => {
modalsModel.pushModal("AboutModal");
});
} catch (_) {
// do nothing
}
const clientAtom: jotai.Atom<Client> = jotai.atom((get) => { const clientAtom: jotai.Atom<Client> = jotai.atom((get) => {
const clientId = get(clientIdAtom); const clientId = get(clientIdAtom);
if (clientId == null) { if (clientId == null) {

View File

@ -64,14 +64,19 @@
// xterm-decoration-top: 2 // xterm-decoration-top: 2
/* modal colors */ /* modal colors */
--modal-bg-color: rgb(26, 28, 26); --modal-bg-color: #232323;
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15); --modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
--modal-border-color: rgba(255, 255, 255, 0.12) /* toggle colors */ --toggle-bg-color: var(--border-color);
/* toggle colors */
--toggle-bg-color: var(--border-color);
--toggle-thumb-color: var(--main-text-color); --toggle-thumb-color: var(--main-text-color);
--toggle-checked-bg-color: var(--accent-color); --toggle-checked-bg-color: var(--accent-color);
/* link color */ /* link color */
--link-color: #58c142; --link-color: #58c142;
/* button colors */
--button-primary-color: #58c142;
--button-secondary-color: rgba(255, 255, 255, 0.1);
--button-danger-color: #d43434;
--button-focus-border-color: rgba(88, 193, 66, 0.8);
} }

View File

@ -68,6 +68,7 @@ declare global {
onUpdaterStatusChange: (callback: (status: UpdaterStatus) => void) => void; onUpdaterStatusChange: (callback: (status: UpdaterStatus) => void) => void;
getUpdaterStatus: () => UpdaterStatus; getUpdaterStatus: () => UpdaterStatus;
installAppUpdate: () => void; installAppUpdate: () => void;
onMenuItemAbout: (callback: () => void) => void;
updateWindowControlsOverlay: (rect: Dimensions) => void; updateWindowControlsOverlay: (rect: Dimensions) => void;
}; };