Compare commits

...

16 Commits

Author SHA1 Message Date
Evan Simkowitz 406afe4a4f save work 2024-03-21 18:01:55 -04:00
Evan Simkowitz be37916ca6 Make a bunch of static elements unselectable (#486)
* Make a bunch of static elements unselectable

* remove unnecessary color
2024-03-21 10:19:29 -07:00
Mike Sawka 0781e6e821 more zsh reinitialization fixes (allow user input during initialization process) (#480)
* fix error logs in scws

* new RpcFollowUpPacketType

* make the rpc/followup handlers generic on the server side -- using new RpcHandlers map and RpcFollowUpPacketType

* rpcinputpacket for passing user input back through to reinit command

* add WAVETERM_DEV env var in dev mode

* remove unused code, ensure mshell and rcfile directory on startup (prevent root clobber with sudo)

* combine all feinput into one function msh.HandleFeInput, and add a new concept of input sinks for special cases (like reinit)

* allow reset to accept user input (to get around interactive initialization problems)

* tone down the selection background highlight color on dark mode.  easier to read selected text

* fix command focus and done focus issues with dynamic (non-run) commands

* add 'module' as a 'rtnstate' command (#478)

* reinitialize shells in parallel, fix timeouts, better error messages
2024-03-20 23:38:05 -07:00
Cole Lashley fb59e094e4 Added formatters for prints (#479) 2024-03-20 16:29:46 -07:00
Mike Sawka 5616c9abbb zsh reinit fixes (#477)
* reset command now initiates and completes async so there is feedback that something is happening when it takes a long time

* switch from standard rpc to rpciter

* checkpoint on reinit -- stream output, stats packet, logging to cmd pty, new endBytes for EOF

* make generic versions of endbytes scanner and channel output funcs

* update bash to use more modern state parsing (tricks learned from zsh)

* verbose mode, fix stats output message

* add a diff when verbose mode is on
2024-03-19 16:38:38 -07:00
Red J Adaya accb74ae0f Fix color regressions (#475)
* fix color regressions

* separate app status vars
2024-03-19 12:22:45 -07:00
Cole Lashley b24c81182b added client settings and removed open ai wording (#474) 2024-03-18 23:43:20 -07:00
Cole Lashley 23fa1c3823 Main View keybindings (#463)
* added all mainviews except session

* add session keybindings

* finished session keybindings

* organized by layer

* remove some webshare code and remove unused session cancel fn

* fixed checkkeypressed bug

* fixed kwargs, bug
2024-03-18 23:41:45 -07:00
Mike Sawka 901dcccaa5 zsh fixes (zmodload, k8s, new var type, track zsh prompt) (#473)
* checkpoint.  track zmodloads, get the expanded PS1 value, change how we store and deal with 'prompt vars', add commands to track k8s context

* fix some warnings, change IsPVar to IsExtVar, checkpoint

* parse zmodloads into state

* restore zmodloads, be more careful around readonly variable setting to avoid errors
2024-03-18 22:51:16 -07:00
Red J Adaya d2f5d87194 Finalize CSS vars (#472)
* remove unused vars

* replace remote status vars with --app-* vars

* replace status indicator vars with --app-* vars

* remove unused vars in default.css

* no need for bg color in view

* use --app* colors for view color

* remove --app-black var

* remove some unused css and divs
2024-03-18 22:00:13 -07:00
Red J Adaya a32bc10981 Datepicker with dark and light theme support (#397)
* datepicker. add electron-rebuild sa dev dependency

* comments

* use <If> component for conditional renders

* invoke callback on month change

* individual inputs for date parts

* keyboard support for input

* more on keyboard support

* dynamic setting of input width based on their text value

* remove error if fromts is out of range

* format is optional

* undo electron-rebuild
2024-03-18 21:44:11 -07:00
Mike Sawka 631b9b89b9 fix nil rct error (#471) 2024-03-18 21:29:39 -07:00
Cole Lashley 0a14a58663 List Config dir if directory is specified (#468)
* implemented list dir for config

* added file stat and path check

* addressed comments, added file not found error
2024-03-18 17:11:13 -07:00
Red J Adaya b656f4ce0f Replace old buttons with Button element (#466)
* init

* model

* pass model

* increase snap threshold

* move saving of state to model. backend implementation

* fix wrong clientdata prop

* right header space for right sidebar toggles

* button component refactor

* set default classname to primary

* remove debugging code

* hide trigger for now

* refactor Button. replace old buttons with Button element

* fix issue with closing alert modal

* make small button a little more round

* hide right sidebar toggle in prod
2024-03-18 16:14:21 -07:00
Red J Adaya 338b9c41c7 Fix wrong dragged tab background theme (#469) 2024-03-18 08:11:39 -07:00
Evan Simkowitz c7b77608a2 Small UI tweaks (#467)
* Extend bottom border for mainview underneath window controls

* Make sidebar button hover color an accent color

* revert 87 to 88

* make mint less bright on light mode

* revert modal border change
2024-03-15 09:54:39 -07:00
68 changed files with 2223 additions and 990 deletions
+18 -9
View File
@@ -87,39 +87,48 @@
},
{
"command": "app:selectTab-1",
"keys": ["Cmd:1"]
"keys": ["Cmd:1"],
"commandStr":"/screen 1"
},
{
"command": "app:selectTab-2",
"keys": ["Cmd:2"]
"keys": ["Cmd:2"],
"commandStr":"/screen 2"
},
{
"command": "app:selectTab-3",
"keys": ["Cmd:3"]
"keys": ["Cmd:3"],
"commandStr":"/screen 3"
},
{
"command": "app:selectTab-4",
"keys": ["Cmd:4"]
"keys": ["Cmd:4"],
"commandStr":"/screen 4"
},
{
"command": "app:selectTab-5",
"keys": ["Cmd:5"]
"keys": ["Cmd:5"],
"commandStr":"/screen 5"
},
{
"command": "app:selectTab-6",
"keys": ["Cmd:6"]
"keys": ["Cmd:6"],
"commandStr":"/screen 6"
},
{
"command": "app:selectTab-7",
"keys": ["Cmd:7"]
"keys": ["Cmd:7"],
"commandStr":"/screen 7"
},
{
"command": "app:selectTab-8",
"keys": ["Cmd:8"]
"keys": ["Cmd:8"],
"commandStr":"/screen 8"
},
{
"command": "app:selectTab-9",
"keys": ["Cmd:9"]
"keys": ["Cmd:9"],
"commandStr":"/screen 9"
},
{
"command": "app:selectTabLeft",
+6 -6
View File
@@ -5,14 +5,14 @@
<base href="../" />
<script charset="UTF-8" src="dist-dev/waveterm.js"></script>
<link rel="stylesheet" href="public/bulma-0.9.4.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/fontawesome.min.css">
<link rel="stylesheet" href="public/fontawesome/css/brands.min.css">
<link rel="stylesheet" href="public/fontawesome/css/solid.min.css">
<link rel="stylesheet" href="public/fontawesome/css/sharp-solid.min.css">
<link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css">
<link rel="stylesheet" href="public/fontawesome/css/fontawesome.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/brands.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/solid.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/sharp-solid.min.css" />
<link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css" />
<link rel="stylesheet" href="dist-dev/waveterm.css" />
<link rel="stylesheet" id="theme-stylesheet" href="public/themes/default.css">
<link rel="stylesheet" id="theme-stylesheet" href="public/themes/default.css" />
</head>
<body>
<div id="measure"></div>
+18 -55
View File
@@ -7,16 +7,6 @@
:root {
--fa-style-family: "Font Awesome 6 Sharp";
/* these variables are overridden by user settings */
/*
--termfontfamily: "Hack";
--termfontsize: 13px;
--termlineheight: 15px;
--termpad: 7px;
--termfontsize-sm: 11px;
--termlineheight-sm: 13px;
*/
/* base fonts */
--base-font: normal 15px / normal "Lato", sans-serif;
@@ -51,13 +41,11 @@
/* global colors */
--app-bg-color: black;
--app-accent-color: rgb(88, 193, 66);
--app-accent-bg-color: rgba(88, 193, 66, 0.2);
--app-error-color: rgb(229, 77, 46);
--app-warning-color: rgb(224, 185, 86);
--app-success-color: rgb(78, 154, 6);
--app-accent-bg-color: rgba(88, 193, 66, 0.25);
--app-text-color: rgb(211, 215, 207);
--app-text-primary-color: rgb(255, 255, 255);
--app-text-secondary-color: rgb(195, 200, 194);
--app-text-disabled-color: rgb(173, 173, 173);
--app-border-color: rgb(51, 51, 51);
--app-maincontent-bg-color: #333;
--app-panel-bg-color: rgba(21, 23, 21, 1);
@@ -66,16 +54,16 @@
--app-icon-hover-color: #fff;
--app-selected-mask-color: rgba(255, 255, 255, 0.06);
/* icon colors */
/* global status colors */
--app-error-color: rgb(229, 77, 46);
--app-warning-color: rgb(224, 185, 86);
--app-success-color: rgb(78, 154, 6);
--app-idle-color: var(--app-text-secondary-color);
/* just for macos */
--app-border-radius-darwin: 10px;
/* global generic colors */
--app-black: rgb(0, 0, 0);
/* scrollbar colors */
/* --scrollbar-background-color: rgba(21, 23, 21, 1); */
--scrollbar-background-color: var(--app-bg-color);
--scrollbar-thumb-color: rgba(255, 255, 255, 0.3);
--scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.5);
@@ -116,28 +104,9 @@
--markdown-bg-color: rgb(35, 35, 35);
--markdown-outline-color: var(--form-element-primary-color);
/* status(remote) colors */
/* todo: all status colors must be unified */
--status-connected-color: var(--app-success-color);
--status-connecting-color: var(--app-warning-color);
--status-error-color: var(--app-error-color);
--status-disconnected-color: var(--app-text-secondary-color);
/* status indicator colors */
/* todo: all status colors must be unified */
--status-indicator-color: var(--app-text-color);
--status-indicator-error: var(--status-error-color);
--status-indicator-success: var(--status-connected-color);
/* status(version) colors */
/* todo: all status colors must be unified */
--status-outdated-color: var(--status-connecting-color);
--status-updated-color: var(--status-connected-color);
/* term status colors */
/* todo: all status colors must be unified */
--term-error-color: var(--status-error-color);
--term-warning-color: var(--status-connecting-color);
--term-error-color: var(--app-error-color);
--term-warning-color: var(--app-warning-color);
/* hotkey colors */
--hotkey-text-color: var(--app-text-secondary-color);
@@ -178,17 +147,6 @@
--line-actions-active-color: rgba(255, 255, 255, 1);
--line-actions-bg-color: rgba(255, 255, 255, 0.15);
/* view colors */
/* todo: bookmarks is a view, colors must be unified with --view* colors */
--bookmarks-text-color: rgb(211, 215, 207);
--bookmarks-textarea-bg-color: rgb(0, 0, 0);
--bookmarks-disabled-text-color: rgb(173, 173, 173);
--bookmarks-control-hover-color: rgb(255, 255, 255);
/* view colors */
--view-error-color: var(--app-error-color);
--view-text-color: var(--app-text-color);
/* table colors */
--table-border-color: rgba(241, 246, 243, 0.15);
--table-thead-border-top-color: rgba(250, 250, 250, 0.1);
@@ -199,9 +157,6 @@
--table-tr-selected-bg-color: #222;
--table-tr-selected-hover-bg-color: #333;
/* session colors */
--session-bg-color: rgba(13, 13, 13, 0.85);
/* cmdinput colors */
--cmdinput-textarea-bg-color: #171717;
--cmdinput-text-error-color: var(--term-red);
@@ -222,5 +177,13 @@
--modal-bg-color: var(--app-bg-color);
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
--logo-button-hover-bg-color: #1e1e1e;
--logo-button-hover-bg-color: var(--app-accent-bg-color);
--xterm-viewport-border-color: rgba(241, 246, 243, 0.15);
--datepicker-cell-hover-bg-color: rgba(255, 255, 255, 0.06);
--datepicker-cell-other-text-color: rgba(255, 255, 255, 0.3);
--datepicker-header-fade-color: rgba(255, 255, 255, 0.4);
--datepicker-year-header-bg-color: rgba(255, 255, 255, 0.2); /* Light grey background */
--datepicker-year-header-border-color: rgba(241, 246, 243, 0.15); /* Light grey border */
}
+11 -5
View File
@@ -23,15 +23,13 @@
/* tab color */
--tab-white: rgb(0, 0, 0, 0.6);
--tab-mint: rgb(70, 235, 156);
/* button colors */
--button-text-color: rgb(50, 50, 50); /* Dark gray for light theme */
--button-primary-color: #fefefe;
--button-secondary-bg-color: rgba(200, 200, 200, 0.3); /* Semi-transparent light gray */
/* view colors */
--view-text-color: var(--app-text-color);
/* table colors */
--table-thead-border-top-color: rgba(0, 0, 0, 0.2);
--table-thead-border-bottom-color: rgba(0, 0, 0, 0.3);
@@ -70,8 +68,16 @@
--line-actions-inactive-color: rgba(0, 0, 0, 0.3);
--line-actions-active-color: rgba(0, 0, 0, 1);
--logo-button-hover-bg-color: #f0f0f0;
--xterm-viewport-border-color: rgba(0, 0, 0, 0.3);
--datepicker-cell-hover-bg-color: #f0f0f0;
--datepicker-cell-other-text-color: rgba(0, 0, 0, 0.3);
--datepicker-header-fade-color: rgba(0, 0, 0, 0.4);
--datepicker-year-header-bg-color: #f5f5f5;
--datepicker-year-header-border-color: #dcdcdc;
/* toggle colors */
--toggle-thumb-color: var(--app-bg-color);
--logo-button-hover-bg-color: #f0f0f0;
}
+1 -1
View File
@@ -27,6 +27,6 @@
--term-cmdtext: #ffffff;
--term-foreground: #d3d7cf;
--term-background: #000000;
--term-selection-background: #ffffff90;
--term-selection-background: #ffffff60;
--term-cursor-accent: #000000;
}
+8 -4
View File
@@ -101,6 +101,7 @@ body input.input {
font-size: 10px;
font-weight: 300;
line-height: 14px;
user-select: none;
}
.text-s1.icon {
@@ -735,28 +736,28 @@ a.a-block {
.status-icon.status-connected {
path,
circle {
fill: var(--status-connected-color);
fill: var(--app-success-color);
}
}
.status-icon.status-connecting {
path,
circle {
fill: var(--status-connecting-color);
fill: var(--app-warning-color);
}
}
.status-icon.status-disconnected {
path,
circle {
fill: var(--status-disconnected-color);
fill: var(--app-idle-color);
}
}
.status-icon.status-error {
path,
circle {
fill: var(--status-error-color);
fill: var(--app-error-color);
}
}
@@ -819,6 +820,7 @@ a.a-block {
display: flex;
flex-direction: row;
align-items: center;
user-select: none;
.info-message {
margin-left: 5px;
}
@@ -867,6 +869,7 @@ a.a-block {
padding-left: 10px;
padding-right: 10px;
font-weight: bold;
user-select: none;
}
.tab-color-icon,
@@ -880,6 +883,7 @@ a.a-block {
display: inline-block;
margin-left: 1em;
min-width: 70px;
user-select: none;
}
.tab-color-select,
+7 -11
View File
@@ -6,11 +6,11 @@
}
.bookmarks-list {
color: var(--bookmarks-text-color);
color: var(--app-text-color);
margin: 4px 10px 5px 5px;
.no-bookmarks {
color: var(--bookmarks-text-color);
color: var(--app-text-color);
padding: 30px 10px 35px 10px;
border-bottom: 1px solid white;
}
@@ -46,21 +46,21 @@
}
label {
color: var(--bookmarks-text-color);
color: var(--app-text-color);
margin-bottom: 4px;
}
textarea {
width: 80%;
min-width: 50%;
color: var(--bookmarks-text-color);
background-color: var(--bookmarks-textarea-bg-color);
color: var(--app-text-color);
background-color: var(--form-element-bg-color);
}
.bookmark-id-div {
display: none;
position: absolute;
color: var(--bookmarks-disabled-text-color);
color: var(--app-text-disabled-color);
right: 5px;
bottom: 2px;
font-size: 0.8em;
@@ -75,7 +75,7 @@
flex-direction: row;
visibility: hidden;
color: var(--bookmarks-text-color);
color: var(--app-text-color);
.bookmark-control:first-child {
margin-left: 0;
@@ -85,10 +85,6 @@
margin-left: 10px;
cursor: pointer;
padding: 2px;
&:hover {
color: var(--bookmarks-control-hover-color);
}
}
}
+45 -3
View File
@@ -5,6 +5,7 @@ import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator";
import { If } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, RemotesModel, getApi } from "@/models";
import { Toggle, InlineSettingsTextEdit, SettingsError, Dropdown } from "@/common/elements";
import { commandRtnHandler, isBlank } from "@/util/util";
@@ -13,6 +14,25 @@ import * as appconst from "@/app/appconst";
import "./clientsettings.less";
import { MainView } from "../common/elements/mainview";
class ClientSettingsKeybindings extends React.Component<{}, {}> {
componentDidMount() {
let clientSettingsViewModel = GlobalModel.clientSettingsViewModel;
let keybindManager = GlobalModel.keybindManager;
keybindManager.registerKeybinding("mainview", "clientsettings", "generic:cancel", (waveEvent) => {
clientSettingsViewModel.closeView();
return true;
});
}
componentWillUnmount() {
GlobalModel.keybindManager.unregisterDomain("clientsettings");
}
render() {
return null;
}
}
@mobxReact.observer
class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hoveredItemId: string }> {
errorMessage: OV<string> = mobx.observable.box(null, { name: "ClientSettings-errorMessage" });
@@ -116,6 +136,12 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
inlineUpdateOpenAIBaseURL(newBaseURL: string): void {
const prtn = GlobalCommandRunner.setClientOpenAISettings({ baseurl: newBaseURL });
commandRtnHandler(prtn, this.errorMessage);
}
@boundMethod
setErrorMessage(msg: string): void {
mobx.action(() => {
@@ -168,6 +194,9 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
return (
<MainView className="clientsettings-view" title="Client Settings" onClose={this.handleClose}>
<If condition={!isHidden}>
<ClientSettingsKeybindings></ClientSettingsKeybindings>
</If>
<div className="content">
<div className="settings-field">
<div className="settings-label">Term Font Size</div>
@@ -232,7 +261,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
</div>
</div>
<div className="settings-field">
<div className="settings-label">OpenAI Token</div>
<div className="settings-label">AI Token</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder=""
@@ -245,7 +274,20 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
</div>
</div>
<div className="settings-field">
<div className="settings-label">OpenAI Model</div>
<div className="settings-label">AI Base URL</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder=""
text={isBlank(openAIOpts.baseurl) ? "openai default" : openAIOpts.baseurl}
value={openAIOpts.baseurl ?? ""}
onChange={this.inlineUpdateOpenAIBaseURL}
maxLength={10}
showIcon={true}
/>
</div>
</div>
<div className="settings-field">
<div className="settings-label">AI Model</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder="gpt-3.5-turbo"
@@ -258,7 +300,7 @@ class ClientSettingsView extends React.Component<{ model: RemotesModel }, { hove
</div>
</div>
<div className="settings-field">
<div className="settings-label">OpenAI MaxTokens</div>
<div className="settings-label">AI MaxTokens</div>
<div className="settings-input">
<InlineSettingsTextEdit
placeholder=""
+40 -41
View File
@@ -1,8 +1,6 @@
.wave-button {
background: none;
color: inherit;
border: none;
font: inherit;
cursor: pointer;
outline: inherit;
display: flex;
@@ -12,64 +10,65 @@
border-radius: 6px;
height: auto;
line-height: 1.5;
display: block;
white-space: nowrap;
user-select: none;
color: var(--form-element-text-color);
background: var(--form-element-primary-color);
i {
fill: var(--form-element-text-color);
}
&.primary {
color: var(--form-element-text-color);
background: var(--form-element-primary-color);
i {
fill: var(--form-element-text-color);
}
}
&.primary.outlined {
background: none;
border: 1px solid var(--form-element-primary-color);
i {
fill: var(--form-element-primary-color);
}
}
&.solid {
color: var(--form-element-text-color);
background: var(--form-element-primary-color);
i {
fill: var(--form-element-text-color);
}
}
&.outlined {
background: none;
border: 1px solid var(--form-element-primary-color);
}
&.ghost {
background: none;
}
&:hover {
color: var(--form-element-text-color);
&.primary.ghost {
background: none;
i {
fill: var(--form-element-primary-color);
}
}
&.secondary {
color: var(--form-element-text-color);
background: none;
color: var(--form-element-text-color);
background: var(--form-element-secondary-color);
box-shadow: none;
&.solid {
color: var(--form-element-text-color);
background: var(--form-element-secondary-color);
box-shadow: none;
i {
fill: var(--form-element-text-color);
}
}
&.outlined {
background: none;
border: 1px solid var(--form-element-secondary-color);
}
&.secondary.outlined {
background: none;
border: 1px solid var(--form-element-text-color);
}
&.ghost {
background: none;
padding: 6px 10px;
&.secondary.ghost {
background: none;
}
i {
fill: var(--form-element-primary-color);
}
}
&.secondary.danger {
color: var(--app-error-color);
}
&.small {
padding: 4px 8px;
font-size: 12px;
border-radius: 3.6px;
}
&.term-inline {
+183
View File
@@ -0,0 +1,183 @@
.day-picker-modal {
background-color: var(--form-element-bg-color);
z-index: 1000;
width: 250px;
height: 293px;
font-size: 13px;
border: 1px solid var(--form-element-border-color);
border-radius: 4px;
color: var(--form-element-text-color);
}
.day-picker-input {
cursor: default;
border: 1px solid var(--form-element-border-color) !important;
height: 34px;
user-select: none;
display: flex;
border-radius: 4px;
justify-content: space-between;
align-items: center;
padding: 0 15px !important;
display: flex;
align-items: center;
justify-content: center;
input {
outline: none !important;
border: none !important;
height: 100%;
padding: 0 !important;
cursor: default !important;
}
.fa-calendar {
margin-left: 10px;
cursor: pointer;
}
}
.day-picker-header {
color: var(--form-element-text-color);
padding: 10px 10px 0;
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
position: sticky;
div {
cursor: default;
font-weight: bold;
user-select: none;
&.fade {
color: var(--datepicker-header-fade-color);
}
}
.arrows {
display: flex;
.wave-button {
padding: 5px !important;
font-size: 18px !important;
cursor: default !important;
}
}
}
.day-header {
display: flex;
color: var(--form-element-text-color);
padding: 0 10px;
.day-header-cell {
flex: 1;
padding: 10px;
color: var(--form-element-text-color);
text-align: center;
font-weight: bold;
user-select: none;
}
}
.day-picker {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0;
padding: 0 10px 10px;
.day {
padding: 10px 4px;
color: var(--form-element-text-color);
text-align: center;
cursor: default;
border-radius: 3px;
&:hover {
background-color: var(--datepicker-cell-hover-bg-color);
}
&.selected {
background-color: var(--form-element-primary-color);
font-weight: bold;
}
}
.other-month {
color: var(--datepicker-cell-other-text-color);
}
}
.year-month-accordion-wrapper {
overflow-y: auto;
height: 243px;
margin-top: 8px;
padding: 0 10px 10px;
.year-month-accordion {
color: var(--form-element-text-color);
}
.year-header {
padding: 5px 10px;
background-color: var(--datepicker-year-header-bg-color);
border-bottom: 1px solid var(--datepicker-year-header-border-color);
margin-bottom: 5px;
font-weight: bold;
text-align: center;
cursor: default;
color: var(--form-element-text-color);
user-select: none;
&:last-child {
margin-bottom: 0;
}
}
.month-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
padding: 10px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
cursor: default;
}
.month {
padding: 5px;
text-align: center;
border-radius: 3px;
color: var(--form-element-text-color);
&:hover {
background-color: var(--datepicker-cell-hover-bg-color);
}
&.selected {
background-color: var(--form-element-primary-color);
}
}
.expanded {
max-height: 500px;
}
}
.dropdown-arrow {
display: inline-block;
margin-left: 5px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid var(--form-element-text-color);
vertical-align: middle;
user-select: none;
&.fade {
border-top: 5px solid var(--datepicker-header-fade-color);
}
}
+446
View File
@@ -0,0 +1,446 @@
import React, { useState, useEffect, useRef, createRef } from "react";
import ReactDOM from "react-dom";
import dayjs from "dayjs";
import cs from "classnames";
import { Button } from "@/elements";
import { If } from "tsx-control-statements/components";
import "./datepicker.less";
interface YearRefs {
[key: number]: React.RefObject<HTMLDivElement>;
}
type DatePickerProps = {
selectedDate: Date;
onSelectDate: (date: Date) => void;
format?: string;
};
const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/YYYY", onSelectDate }) => {
const [isOpen, setIsOpen] = useState(false);
const [selDate, setSelDate] = useState(dayjs(selectedDate)); // Initialize with dayjs object
const [showYearAccordion, setShowYearAccordion] = useState(false);
const [expandedYear, setExpandedYear] = useState<number | null>(selDate.year());
const yearRefs = useRef<YearRefs>({});
const wrapperRef = useRef<HTMLDivElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
const calendarIconRef = useRef(null);
const inputRefs = useRef({ YYYY: null, MM: null, DD: null });
// Extract delimiter using regex
const delimiter = format.replace(/[0-9YMD]/g, "")[0] || "/";
// Split format and create state for each part
const formatParts = format.split(delimiter);
const [dateParts, setDateParts] = useState({
YYYY: selDate.format("YYYY"),
MM: selDate.format("MM"),
DD: selDate.format("DD"),
});
useEffect(() => {
inputRefs.current = {
YYYY: createRef(),
MM: createRef(),
DD: createRef(),
};
}, []);
useEffect(() => {
if (showYearAccordion && expandedYear && yearRefs.current[expandedYear]) {
yearRefs.current[expandedYear].current?.scrollIntoView({
block: "nearest",
});
}
}, [showYearAccordion, expandedYear]);
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
useEffect(() => {
setDateParts({
YYYY: selDate.format("YYYY"),
MM: selDate.format("MM"),
DD: selDate.format("DD"),
});
}, [selDate]);
const handleClickOutside = (event: MouseEvent) => {
// Check if the click is on the calendar icon
if (calendarIconRef.current && calendarIconRef.current.contains(event.target as Node)) {
// Click is on the calendar icon, do nothing
return;
}
// Check if the click is outside the modal
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
setIsOpen(false); // Close the modal
}
};
const handleDayClick = (date: Date) => {
const newDate = dayjs(date);
setSelDate(newDate); // Update selDate with the new dayjs object
onSelectDate && onSelectDate(date); // Call parent's onSelectDate
setIsOpen(false); // Close the picker
};
const changeMonth = (delta: number) => {
const newDate = selDate.add(delta, "month");
setSelDate(newDate);
onSelectDate && onSelectDate(newDate.toDate());
};
const renderHeader = () => {
return (
<div className="day-picker-header">
<div
className={cs({ fade: showYearAccordion })}
onClick={() => {
if (!showYearAccordion) {
setExpandedYear(selDate.year()); // Set expandedYear when opening accordion
}
setShowYearAccordion(!showYearAccordion);
}}
>
{selDate.format("MMMM YYYY")}
<span className={cs("dropdown-arrow", { fade: showYearAccordion })}></span>
</div>
<If condition={!showYearAccordion}>
<div className="arrows">
<Button className="secondary ghost" onClick={() => changeMonth(-1)}>
&uarr;
</Button>
<Button className="secondary ghost" onClick={() => changeMonth(1)}>
&darr;
</Button>
</div>
</If>
</div>
);
};
const renderDayHeaders = () => {
const daysOfWeek = ["S", "M", "T", "W", "T", "F", "S"]; // First letter of each day
return (
<div className="day-header">
{daysOfWeek.map((day, i) => (
<div key={`${day}-${i}`} className="day-header-cell">
{day}
</div>
))}
</div>
);
};
const renderDays = () => {
const days = [];
const startDay = selDate.startOf("month");
const endDay = selDate.endOf("month");
const startDate = startDay.day(); // 0 for Sunday, 1 for Monday, ..., 6 for Saturday
// Previous month's filler days
const previousMonth = startDay.subtract(1, "month");
const daysInPreviousMonth = previousMonth.daysInMonth();
for (let i = daysInPreviousMonth - startDate + 1; i <= daysInPreviousMonth; i++) {
const dayDate = previousMonth.date(i);
days.push(
<div
key={`prev-month-day-${i}`}
className="day other-month"
onClick={() => handleDayClick(dayDate.toDate())}
>
{i}
</div>
);
}
// Current month's days
for (
let dayCount = 1;
startDay.add(dayCount - 1, "day").isBefore(endDay) ||
startDay.add(dayCount - 1, "day").isSame(endDay, "day");
dayCount++
) {
const currentDate = startDay.add(dayCount - 1, "day");
days.push(
<div
key={dayCount}
className={`day ${selDate.isSame(currentDate, "day") ? "selected" : ""}`}
onClick={() => handleDayClick(currentDate.toDate())}
>
{dayCount}
</div>
);
}
// Next month's filler days
while (days.length < 42) {
const fillerDayCount = days.length - daysInPreviousMonth - endDay.date();
const dayDate = endDay.add(fillerDayCount + 1, "day");
days.push(
<div
key={`next-month-day-${dayDate.format("YYYY-MM-DD")}`}
className="day other-month"
onClick={() => handleDayClick(dayDate.toDate())}
>
{dayDate.date()}
</div>
);
}
return days;
};
const calculatePosition = (): React.CSSProperties => {
if (wrapperRef.current) {
const rect = wrapperRef.current.getBoundingClientRect();
return {
position: "absolute",
top: `${rect.bottom + window.scrollY + 2}px`,
left: `${rect.left + window.scrollX}px`,
};
}
return {};
};
const populateYears = () => {
const currentYear = dayjs().year();
const startYear = currentYear - 10;
const endYear = currentYear + 10;
const yearsRange = [];
for (let year = startYear; year <= endYear; year++) {
yearsRange.push(year);
yearRefs.current[year] = React.createRef();
}
return yearsRange;
};
const handleMonthYearSelect = (month: number, year: number) => {
const newDate = dayjs(new Date(year, month - 1));
setSelDate(newDate);
setShowYearAccordion(false); // Close accordion
onSelectDate && onSelectDate(newDate.toDate());
};
const renderYearMonthAccordion = () => {
const years = populateYears();
const currentYear = selDate.year();
return (
<div className="year-month-accordion-wrapper">
<div className="year-month-accordion">
{years.map((year) => (
<div key={year} ref={yearRefs.current[year]}>
<div
className="year-header"
data-year={year}
onClick={() => setExpandedYear(year === expandedYear ? null : year)}
>
{year}
</div>
<If condition={expandedYear === year}>
<div
className={cs("month-container", {
expanded: expandedYear === year,
})}
>
{Array.from({ length: 12 }, (_, i) => i + 1).map((month) => (
<div
key={month}
className={cs("month", {
selected: year === currentYear && month === selDate.month() + 1,
})}
onClick={() => handleMonthYearSelect(month, year)}
>
{dayjs(new Date(year, month - 1)).format("MMM")}
</div>
))}
</div>
</If>
</div>
))}
</div>
</div>
);
};
const toggleModal = () => {
setIsOpen((prevIsOpen) => !prevIsOpen);
setShowYearAccordion(false);
};
const dayPickerModal = isOpen
? ReactDOM.createPortal(
<div ref={modalRef} className="day-picker-modal" style={calculatePosition()}>
{renderHeader()}
{showYearAccordion && renderYearMonthAccordion()}
<If condition={!showYearAccordion}>
<>
{renderDayHeaders()}
<div className="day-picker">{renderDays()}</div>
</>
</If>
</div>,
document.getElementById("app")!
)
: null;
const handleDatePartChange = (part, value) => {
const newDateParts = { ...dateParts, [part]: value };
setDateParts(newDateParts);
// Construct a new date from the updated parts
const newDate = dayjs(`${newDateParts.YYYY}-${newDateParts.MM}-${newDateParts.DD}`);
if (newDate.isValid()) {
onSelectDate(newDate.toDate()); // Call onSelectDate with the new date
}
};
const handleArrowNavigation = (event, currentPart) => {
const currentIndex = formatParts.indexOf(currentPart);
let targetInput;
if (event.key === "ArrowLeft" && currentIndex > 0) {
targetInput = inputRefs.current[formatParts[currentIndex - 1]].current;
} else if (event.key === "ArrowRight" && currentIndex < formatParts.length - 1) {
targetInput = inputRefs.current[formatParts[currentIndex + 1]].current;
}
if (targetInput) {
targetInput.focus();
}
};
const handleKeyDown = (event, currentPart) => {
const key = event.key;
if (key === "ArrowLeft" || key === "ArrowRight") {
// Handle arrow navigation without selecting text
handleArrowNavigation(event, currentPart);
return;
}
if (key === " ") {
// Handle spacebar press to toggle the modal
toggleModal();
return;
}
if (key.match(/[0-9]/)) {
// Handle numeric keys
event.preventDefault();
const maxLength = currentPart === "YYYY" ? 4 : 2;
const newValue = event.target.value.length < maxLength ? event.target.value + key : key;
let selectionTimeoutId = null;
handleDatePartChange(currentPart, newValue);
// Clear any existing timeout
if (selectionTimeoutId !== null) {
clearTimeout(selectionTimeoutId);
}
// Re-focus and select the input after state update
selectionTimeoutId = setTimeout(() => {
event.target.focus();
event.target.select();
}, 0);
}
};
const handleFocus = (event) => {
event.target.select();
};
// Prevent use from selecting text in the input
const handleMouseDown = (event) => {
event.preventDefault();
handleFocus(event);
};
const handleIconKeyDown = (event) => {
if (event.key === "Enter") {
toggleModal();
}
};
const setInputWidth = (inputRef, value) => {
const span = document.createElement("span");
document.body.appendChild(span);
span.style.font = "inherit";
span.style.visibility = "hidden";
span.style.position = "absolute";
span.textContent = value;
const textWidth = span.offsetWidth;
document.body.removeChild(span);
if (inputRef.current) {
inputRef.current.style.width = `${textWidth}px`;
}
};
useEffect(() => {
// This timeout ensures that the effect runs after the DOM updates
const timeoutId = setTimeout(() => {
formatParts.forEach((part) => {
const inputRef = inputRefs.current[part];
if (inputRef && inputRef.current) {
setInputWidth(inputRef, dateParts[part]);
}
});
}, 0);
return () => clearTimeout(timeoutId); // Cleanup timeout on unmount
}, []);
const renderDatePickerInput = () => {
return (
<div className="day-picker-input">
{formatParts.map((part, index) => {
const inputRef = inputRefs.current[part];
return (
<React.Fragment key={part}>
{index > 0 && <span>{delimiter}</span>}
<input
readOnly
ref={inputRef}
type="text"
value={dateParts[part]}
onChange={(e) => handleDatePartChange(part, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, part)}
onMouseDown={handleMouseDown}
onFocus={handleFocus}
maxLength={part === "YYYY" ? 4 : 2}
className="date-input"
placeholder={part}
/>
</React.Fragment>
);
})}
<i
ref={calendarIconRef}
className="fa-sharp fa-regular fa-calendar"
onClick={toggleModal}
onKeyDown={handleIconKeyDown}
tabIndex={0} // Makes the icon focusable
role="button" // Semantic role for accessibility
aria-label="Toggle date picker" // Accessible label for screen readers
/>
</div>
);
};
return (
<div ref={wrapperRef}>
{renderDatePickerInput()}
{dayPickerModal}
</div>
);
};
export { DatePicker };
+1
View File
@@ -20,6 +20,7 @@
transition: all 0.3s;
color: var(--form-element-label-color);
line-height: 10px;
user-select: none;
&.float {
font-size: 10px;
+1
View File
@@ -17,3 +17,4 @@ export { TextField } from "./textfield";
export { Toggle } from "./toggle";
export { Tooltip } from "./tooltip";
export { TabIcon } from "./tabicon";
export { DatePicker } from "./datepicker";
+9 -5
View File
@@ -9,6 +9,10 @@
display: none;
}
.header-container {
border-bottom: 1px solid var(--app-border-color);
}
.header {
-webkit-app-region: drag;
display: flex;
@@ -18,7 +22,7 @@
vertical-align: middle;
padding: 0 10px 0 10px;
margin: 0;
border-bottom: 1px solid var(--app-border-color);
user-select: none;
.title {
font-size: var(--title-font-size);
@@ -42,10 +46,10 @@
// This ensures the tab bar does not collide with the floating logo. The floating logo sits above the sidebar when it is not collapsed, so no additional margin is needed in that case.
// More margin is given on macOS to account for the traffic light buttons
#main.platform-darwin.mainsidebar-collapsed .header {
margin-left: var(--floating-logo-width-darwin);
#main.platform-darwin.mainsidebar-collapsed .header-container {
padding-left: var(--floating-logo-width-darwin);
}
#main:not(.platform-darwin).mainsidebar-collapsed .header {
margin-left: var(--floating-logo-width);
#main:not(.platform-darwin).mainsidebar-collapsed .header-container {
padding-left: var(--floating-logo-width);
}
+1 -1
View File
@@ -23,7 +23,7 @@ class MainView extends React.Component<{
className={cn("mainview", this.props.className)}
style={{ maxWidth: `calc(100vw - ${maxWidthSubtractor}px)` }}
>
<div className="header-container bottom-border">
<div className="header-container">
<header className="header">
<div className="title text-primary">{this.props.title}</div>
<div className="close-div hoverEffect" title="Close (Escape)" onClick={this.props.onClose}>
+1
View File
@@ -44,6 +44,7 @@
justify-content: space-between;
line-height: 20px;
border-bottom: 1px solid var(--modal-header-bottom-border-color);
user-select: none;
.wave-modal-title {
color: var(--app-text-primary-color);
+4 -4
View File
@@ -11,18 +11,18 @@
}
.dot.green {
background-color: var(--status-connected-color);
background-color: var(--app-success-color);
}
.dot.red {
background-color: var(--status-error-color);
background-color: var(--app-error-color);
}
.dot.gray {
background-color: var(--status-disconnected-color);
background-color: var(--app-idle-color);
}
.dot.yellow {
background-color: var(--status-connecting-color);
background-color: var(--app-warning-color);
}
}
+3 -2
View File
@@ -39,6 +39,7 @@
transition: all 0.3s;
color: var(--form-element-label-color);
line-height: 10px;
user-select: none;
&.float {
font-size: 10px;
@@ -65,8 +66,8 @@
padding: 5px 16px 5px 0;
}
input::placeholder {
color: var(--form-element-placeholder-color);
&:placeholder-shown {
user-select: none;
}
}
}
+4 -4
View File
@@ -73,18 +73,18 @@ The following accounts for a debounce in the status indicator. We will only disp
visibility: hidden;
}
.spin #spinner {
stroke: var(--status-indicator-color);
stroke: var(--app-text-color);
}
&.error #indicator {
visibility: visible;
fill: var(--status-indicator-error);
fill: var(--app-error-color);
}
&.success #indicator {
visibility: visible;
fill: var(--status-indicator-success);
fill: var(--app-success-color);
}
&.output #indicator {
visibility: visible;
fill: var(--status-indicator-color);
fill: var(--app-text-color);
}
}
+2 -2
View File
@@ -70,7 +70,7 @@
margin-bottom: 5px;
i {
color: var(--status-updated-color);
color: var(--app-success-color);
}
}
}
@@ -78,7 +78,7 @@
.status.outdated {
div {
i {
color: var(--status-outdated-color);
color: var(--app-warning-color);
}
}

Some files were not shown because too many files have changed in this diff Show More