From 964c422a02bc72084fcde833a710195d8e8d3def Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Tue, 20 Aug 2024 06:49:40 +0800 Subject: [PATCH] about modal (#244) --- emain/emain.ts | 5 +- emain/preload.ts | 1 + frontend/app/element/button.less | 198 ++++++++++---------------- frontend/app/element/button.tsx | 22 +-- frontend/app/element/linkbutton.less | 12 ++ frontend/app/element/linkbutton.tsx | 37 +++++ frontend/app/modals/about.less | 61 ++++++++ frontend/app/modals/about.tsx | 61 ++++++++ frontend/app/modals/modal.less | 64 +++++---- frontend/app/modals/modal.tsx | 51 ++++--- frontend/app/modals/modalregistry.tsx | 2 + frontend/app/modals/tos.less | 3 + frontend/app/store/global.ts | 9 ++ frontend/app/theme.less | 11 +- frontend/types/custom.d.ts | 1 + 15 files changed, 353 insertions(+), 185 deletions(-) create mode 100644 frontend/app/element/linkbutton.less create mode 100644 frontend/app/element/linkbutton.tsx create mode 100644 frontend/app/modals/about.less create mode 100644 frontend/app/modals/about.tsx diff --git a/emain/emain.ts b/emain/emain.ts index e02edd32b..ee76d548d 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -690,7 +690,10 @@ function getAppMenu() { ]; const appMenu: Electron.MenuItemConstructorOptions[] = [ { - role: "about", + label: "About Wave Terminal", + click: (_, window) => { + window?.webContents.send("menu-item-about"); + }, }, { label: "Check for Updates", diff --git a/emain/preload.ts b/emain/preload.ts index 866f1e8f7..77d44dc6a 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -24,6 +24,7 @@ contextBridge.exposeInMainWorld("api", { onUpdaterStatusChange: (callback) => ipcRenderer.on("app-update-status", (_event, status) => callback(status)), getUpdaterStatus: () => ipcRenderer.sendSync("get-app-update-status"), installAppUpdate: () => ipcRenderer.send("install-app-update"), + onMenuItemAbout: (callback) => ipcRenderer.on("menu-item-about", callback), updateWindowControlsOverlay: (rect) => ipcRenderer.send("update-window-controls-overlay", rect), }); diff --git a/frontend/app/element/button.less b/frontend/app/element/button.less index 7186472d9..03f49e8c3 100644 --- a/frontend/app/element/button.less +++ b/frontend/app/element/button.less @@ -2,150 +2,102 @@ /* SPDX-License-Identifier: Apache-2.0 */ .button { - background: var(--accent-color); - border: none; - cursor: pointer; - 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; + padding: 1px; + border: 1px solid transparent; + border-radius: 8px; - color: var(--main-text-color); - - i { - fill: var(--main-text-color); + &:focus, + &.focus { + border: 1px solid var(--button-focus-border-color); } - &.primary, - &.secondary { - color: var(--main-text-color); + .button-inner { 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 { fill: var(--main-text-color); } - } - &.primary.danger { - background: var(--error-color); - } - - &.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 { + &.primary, + &.secondary { 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); } - } - &.greytext { - color: var(--secondary-text-color); - } + &.primary.warning { + background: #e6ba1e; + color: #000000; + } - &.primary.ghost { - background: none; + &.primary.ghost { + background: none; - i { - fill: var(--accent-color); + i { + 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 { - fill: var(--app-error-color); - } + /* + * customs styles here + */ + .border-radius-4 { + border-radius: 4px; } - &.secondary { - background: var(--highlight-bg-color); + .vertical-padding-2 { + padding-top: 2px; + padding-bottom: 2px; } - &.secondary.outlined { - background: none; - border: 1px solid var(--main-text-color); - } - - &.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; + .horizontal-padding-10 { + padding-left: 10px; + padding-right: 10px; } } - -.border-radius-4 { - border-radius: 4px; -} - -.vertical-padding-2 { - padding-top: 2px; - padding-bottom: 2px; -} - -.horizontal-padding-10 { - padding-left: 10px; - padding-right: 10px; -} diff --git a/frontend/app/element/button.tsx b/frontend/app/element/button.tsx index 4f3a04559..ef650d1c0 100644 --- a/frontend/app/element/button.tsx +++ b/frontend/app/element/button.tsx @@ -14,16 +14,18 @@ const Button = React.memo(({ className = "primary", children, disabled, ...props ); return ( - +
+ +
); }); diff --git a/frontend/app/element/linkbutton.less b/frontend/app/element/linkbutton.less new file mode 100644 index 000000000..954d638fb --- /dev/null +++ b/frontend/app/element/linkbutton.less @@ -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; + } + } +} diff --git a/frontend/app/element/linkbutton.tsx b/frontend/app/element/linkbutton.tsx new file mode 100644 index 000000000..3343d15e5 --- /dev/null +++ b/frontend/app/element/linkbutton.tsx @@ -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) => void; +} + +const LinkButton = ({ leftIcon, rightIcon, children, className, ...rest }: LinkButtonProps) => { + return ( + + + {leftIcon && {leftIcon}} + {children} + {rightIcon && {rightIcon}} + + + ); +}; + +export { LinkButton }; diff --git a/frontend/app/modals/about.less b/frontend/app/modals/about.less new file mode 100644 index 000000000..9b4a2b302 --- /dev/null +++ b/frontend/app/modals/about.less @@ -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; + } + } + } + } +} diff --git a/frontend/app/modals/about.tsx b/frontend/app/modals/about.tsx new file mode 100644 index 000000000..de4b8c928 --- /dev/null +++ b/frontend/app/modals/about.tsx @@ -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 ( + modalsModel.popModal()}> +
+
+ +
Wave Terminal
+
+ Open-Source AI-Native Terminal +
+ Built for Seamless Workflows +
+
+
Client Version 0.1.8 (20240615-002636)
+
+ } + > + Github + + } + > + Website + + } + > + Acknowledgements + +
+
© {currentDate.getFullYear()} Command Line Inc.
+
+
+ ); +}; + +AboutModal.displayName = "AboutModal"; + +export { AboutModal }; diff --git a/frontend/app/modals/modal.less b/frontend/app/modals/modal.less index dc9eff73d..fabac766f 100644 --- a/frontend/app/modals/modal.less +++ b/frontend/app/modals/modal.less @@ -24,51 +24,61 @@ } .modal { + position: relative; z-index: var(--zindex-modal); display: flex; flex-direction: column; align-items: flex-start; - gap: 16px; - border-radius: 10px; + gap: 32px; + padding: 24px 16px 16px; + border-radius: 8px; + border: 0.5px solid var(--modal-border-color); background: var(--modal-bg-color); - border: 1px solid var(--border-color); - box-shadow: 0px 5px 5px 5px rgba(0, 0, 0, 0.1); + box-shadow: 0px 8px 32px 0px rgba(0, 0, 0, 0.25); - .modal-header { - width: 100%; + .header-content-wrapper { display: flex; - align-items: center; - padding: 12px 14px 12px 20px; - justify-content: space-between; - line-height: 20px; - border-bottom: 1px solid var(--modal-header-bottom-border-color); - user-select: none; + flex-direction: column; + gap: 8px; + width: 100%; - .modal-title { - color: var(--main-text-color); - font-size: var(--title-font-size); - } + .modal-header { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + user-select: none; - button { - padding-right: 4px !important; - i { - font-size: 18px; + .modal-title { + color: var(--main-text-color); + font-size: var(--title-font-size); + } + + button { + position: absolute; + right: 8px; + top: 8px; + padding: 8px 16px; + + i { + font-size: 18px; + } } } - } - - .modal-body { - width: 100%; - padding: 0px 20px; + .modal-content { + width: 100%; + padding: 0px 20px; + } } .modal-footer { display: flex; justify-content: flex-end; 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; } } diff --git a/frontend/app/modals/modal.tsx b/frontend/app/modals/modal.tsx index 6a891ad20..fca72e697 100644 --- a/frontend/app/modals/modal.tsx +++ b/frontend/app/modals/modal.tsx @@ -2,19 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 import { Button } from "@/app/element/button"; +import clsx from "clsx"; import ReactDOM from "react-dom"; import "./modal.less"; interface ModalHeaderProps { - title: React.ReactNode; description?: string; onClose?: () => void; } -const ModalHeader = ({ onClose, title, description }: ModalHeaderProps) => ( +const ModalHeader = ({ onClose, description }: ModalHeaderProps) => (
- {typeof title === "string" ?

{title}

: title} {description &&

{description}

} {onClose && ( - )} - {onOk && ( - - )} - -); +const ModalFooter = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }: ModalFooterProps) => { + return ( +
+ {onCancel && ( + + )} + {onOk && ( + + )} +
+ ); +}; interface ModalProps { title: string; @@ -81,13 +82,21 @@ const Modal = ({ }: ModalProps) => { const renderBackdrop = (onClick) =>
; + const renderFooter = () => { + return onOk || onCancel; + }; + const renderModal = () => (
{renderBackdrop(onClickBackdrop)} -
- - {children} - +
+
+ + {children} +
+ {renderFooter() && ( + + )}
); diff --git a/frontend/app/modals/modalregistry.tsx b/frontend/app/modals/modalregistry.tsx index 3f5f48fdb..be9d0b71e 100644 --- a/frontend/app/modals/modalregistry.tsx +++ b/frontend/app/modals/modalregistry.tsx @@ -1,12 +1,14 @@ // Copyright 2023, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { AboutModal } from "./about"; import { TosModal } from "./tos"; import { UserInputModal } from "./userinputmodal"; const modalRegistry: { [key: string]: React.ComponentType } = { [TosModal.displayName || "TosModal"]: TosModal, [UserInputModal.displayName || "UserInputModal"]: UserInputModal, + [AboutModal.displayName || "AboutModal"]: AboutModal, }; export const getModalComponent = (key: string): React.ComponentType | undefined => { diff --git a/frontend/app/modals/tos.less b/frontend/app/modals/tos.less index 756326143..18f2ca707 100644 --- a/frontend/app/modals/tos.less +++ b/frontend/app/modals/tos.less @@ -1,5 +1,6 @@ .tos-modal { width: 640px; + border-radius: 10px; .modal-inner { padding: 40px 76px; @@ -16,6 +17,8 @@ .logo { margin-bottom: 10px; + display: flex; + justify-content: center; } .modal-title { diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index e90b9188a..c0a1049a5 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -62,6 +62,15 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { // do nothing } + const showAboutModalAtom = jotai.atom(false) as jotai.PrimitiveAtom; + try { + getApi().onMenuItemAbout(() => { + modalsModel.pushModal("AboutModal"); + }); + } catch (_) { + // do nothing + } + const clientAtom: jotai.Atom = jotai.atom((get) => { const clientId = get(clientIdAtom); if (clientId == null) { diff --git a/frontend/app/theme.less b/frontend/app/theme.less index d0091a966..527432bed 100644 --- a/frontend/app/theme.less +++ b/frontend/app/theme.less @@ -64,14 +64,19 @@ // xterm-decoration-top: 2 /* 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-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-checked-bg-color: var(--accent-color); /* link color */ --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); } diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index d8172459b..309289ca2 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -68,6 +68,7 @@ declare global { onUpdaterStatusChange: (callback: (status: UpdaterStatus) => void) => void; getUpdaterStatus: () => UpdaterStatus; installAppUpdate: () => void; + onMenuItemAbout: (callback: () => void) => void; updateWindowControlsOverlay: (rect: Dimensions) => void; };