Compare commits

..

15 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
67 changed files with 2211 additions and 980 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>
+16 -53
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;
@@ -52,12 +42,10 @@
--app-bg-color: black;
--app-accent-color: rgb(88, 193, 66);
--app-accent-bg-color: rgba(88, 193, 66, 0.25);
--app-error-color: rgb(229, 77, 46);
--app-warning-color: rgb(224, 185, 86);
--app-success-color: rgb(78, 154, 6);
--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);
@@ -223,4 +178,12 @@
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
--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 */
}
+10 -3
View File
@@ -30,9 +30,6 @@
--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);
@@ -71,6 +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);
}
+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";
+1
View File
@@ -22,6 +22,7 @@
vertical-align: middle;
padding: 0 10px 0 10px;
margin: 0;
user-select: none;
.title {
font-size: var(--title-font-size);
+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);
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ import "./alert.less";
class AlertModal extends React.Component<{}, {}> {
@boundMethod
closeModal(): void {
GlobalModel.cancelAlert();
GlobalModel.modalsModel.popModal(() => GlobalModel.cancelAlert());
}
@boundMethod

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