mirror of
https://github.com/wavetermdev/backup.git
synced 2026-04-22 15:26:58 -07:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 406afe4a4f | |||
| be37916ca6 | |||
| 0781e6e821 | |||
| fb59e094e4 | |||
| 5616c9abbb | |||
| accb74ae0f | |||
| b24c81182b | |||
| 23fa1c3823 | |||
| 901dcccaa5 | |||
| d2f5d87194 | |||
| a32bc10981 | |||
| 631b9b89b9 | |||
| 0a14a58663 | |||
| b656f4ce0f | |||
| 338b9c41c7 | |||
| c7b77608a2 | |||
| 5f6b671ea8 | |||
| 318aae18d5 | |||
| 44535d2964 | |||
| 8851c8e2d6 | |||
| 07e0b53b17 | |||
| 61c9d21014 | |||
| bfafb9e490 |
@@ -1,30 +0,0 @@
|
||||
name: Publish a new release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
env:
|
||||
NODE_VERSION: "21.5.0"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
cache: "yarn"
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(node -e 'console.log(require("./version.js"))')
|
||||
echo "WAVETERM_VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
- name: Download staged artifacts
|
||||
run: |
|
||||
. ./buildres/download-staged-artifacts.sh
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.S3_USERID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.S3_SECRETKEY }}"
|
||||
AWS_DEFAULT_REGION: us-west-2
|
||||
- name: Generate Flatpak manifest
|
||||
run: node buildres/flatpak/generate-flatpak-manifest.js
|
||||
@@ -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",
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
app-id: com.visualstudio.code
|
||||
default-branch: stable
|
||||
runtime: org.freedesktop.Sdk
|
||||
runtime-version: '23.08'
|
||||
sdk: org.freedesktop.Sdk
|
||||
base: org.electronjs.Electron2.BaseApp
|
||||
base-version: '23.08'
|
||||
command: code
|
||||
tags: [proprietary]
|
||||
separate-locales: false
|
||||
finish-args:
|
||||
- --require-version=0.10.3
|
||||
- --share=network
|
||||
- --share=ipc
|
||||
- --socket=x11
|
||||
- --socket=pulseaudio
|
||||
- --socket=ssh-auth
|
||||
- --device=all
|
||||
- --allow=devel
|
||||
- --filesystem=host
|
||||
- --env=NPM_CONFIG_GLOBALCONFIG=/app/etc/npmrc
|
||||
- --env=LD_LIBRARY_PATH=/app/lib
|
||||
# required to fix cursor scaling on wayland
|
||||
- --env=XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons
|
||||
- --system-talk-name=org.freedesktop.login1
|
||||
- --talk-name=org.freedesktop.Notifications
|
||||
- --talk-name=org.freedesktop.secrets
|
||||
- --talk-name=org.freedesktop.Flatpak
|
||||
- --talk-name=com.canonical.AppMenu.Registrar
|
||||
- --talk-name=com.canonical.AppMenu.Registrar.*
|
||||
add-extensions:
|
||||
com.visualstudio.code.tool:
|
||||
directory: tools
|
||||
subdirectories: true
|
||||
version: '23.08'
|
||||
add-ld-path: lib
|
||||
no-autodownload: true
|
||||
cleanup:
|
||||
- /include
|
||||
- /lib/pkgconfig
|
||||
- /share/gtk-doc
|
||||
- '*.la'
|
||||
modules:
|
||||
- shared-modules/libusb/libusb.json
|
||||
|
||||
- name: vscode
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- install -D code.sh /app/bin/code
|
||||
- install -Dm644 vscode_64.png /app/share/icons/hicolor/64x64/apps/com.visualstudio.code.png
|
||||
- install -Dm644 vscode_128.png /app/share/icons/hicolor/128x128/apps/com.visualstudio.code.png
|
||||
- install -Dm644 vscode_256.png /app/share/icons/hicolor/256x256/apps/com.visualstudio.code.png
|
||||
- install -Dm644 vscode_512.png /app/share/icons/hicolor/512x512/apps/com.visualstudio.code.png
|
||||
- install -Dm644 com.visualstudio.code.metainfo.xml -t /app/share/metainfo
|
||||
- install -Dm644 com.visualstudio.code.desktop -t /app/share/applications
|
||||
- install -Dm644 com.visualstudio.code-url-handler.desktop -t /app/share/applications
|
||||
- install -Dm644 com.visualstudio.code-workspace.xml -t /app/share/mime/packages
|
||||
- install -Dm644 npmrc -t /app/etc
|
||||
- install -Dm644 flatpak-warning.txt -t /app/share/vscode
|
||||
- install -D apply_extra -t /app/bin
|
||||
- cp /usr/bin/ar /app/bin
|
||||
- ARCH_TRIPLE=$(gcc --print-multiarch) && cp /usr/lib/${ARCH_TRIPLE}/libbfd-*.so
|
||||
/app/lib
|
||||
- ARCH_TRIPLE=$(gcc --print-multiarch) && ln -s /usr/lib/${ARCH_TRIPLE}/libtinfo.so/app/lib/libtinfo.so.5
|
||||
- mkdir /app/tools
|
||||
sources:
|
||||
- type: script
|
||||
dest-filename: apply_extra
|
||||
commands:
|
||||
- ar x code.deb
|
||||
- tar xf data.tar.xz
|
||||
- mv usr/share/code vscode
|
||||
- rm -r code.deb control.tar.* data.tar.xz debian-binary usr
|
||||
- type: file
|
||||
path: code.sh
|
||||
- type: file
|
||||
path: flatpak-warning.txt
|
||||
- type: file
|
||||
path: npmrc
|
||||
- type: file
|
||||
path: com.visualstudio.code.metainfo.xml
|
||||
- type: file
|
||||
path: com.visualstudio.code.desktop
|
||||
- type: file
|
||||
path: com.visualstudio.code-url-handler.desktop
|
||||
- type: file
|
||||
path: com.visualstudio.code-workspace.xml
|
||||
- type: file
|
||||
path: icons/vscode_64.png
|
||||
- type: file
|
||||
path: icons/vscode_128.png
|
||||
- type: file
|
||||
path: icons/vscode_256.png
|
||||
- type: file
|
||||
path: icons/vscode_512.png
|
||||
- type: extra-data
|
||||
filename: code.deb
|
||||
only-arches: [x86_64]
|
||||
url: <to-replace>
|
||||
sha256: <to-replace>
|
||||
size: <to-replace>
|
||||
x-checker-data:
|
||||
type: electron-updater
|
||||
url: https://dl.waveterm.dev/releases/latest-linux.yml
|
||||
is-main-source: true
|
||||
|
||||
- name: host-spawn
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- install -Dm755 host-spawn /app/bin/host-spawn
|
||||
sources:
|
||||
- type: file
|
||||
url: https://github.com/1player/host-spawn/releases/download/1.5.0/host-spawn-x86_64
|
||||
sha256: dbf67e7e111c4fe1edb0c642cbb4193064ca5b384aeb1054fc2befba6ed88b83
|
||||
dest-filename: host-spawn
|
||||
only-arches: [x86_64]
|
||||
@@ -1,16 +0,0 @@
|
||||
// Updates the Flatpak manifest with the latest values for the staged artifact
|
||||
// Usage: node generate-flatpak-manifest.js <path-to-staging-directory>
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const yaml = require("yaml");
|
||||
|
||||
async function generateFlatpakManifest(stagingDir) {
|
||||
const latestYmlPath = path.join(stagingDir, "latest-linux.yml");
|
||||
const latestYml = yaml.parse(fs.readFileSync(latestYmlPath, "utf8"));
|
||||
const manifestPath = path.join(stagingDir, "dev.commandline.waveterm.yml");
|
||||
const manifestYml = yaml.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
const latestDeb = latestYml.files.find((file) => file.url.endsWith(".deb"));
|
||||
const latestDebUrl = `https://dl.waveterm.dev/releases/${latestDeb.url}`;
|
||||
const latestDebSha512 = latestDeb.sha512;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"productName": "Wave",
|
||||
"description": "An Open-Source, AI-Native, Terminal Built for Seamless Workflows",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"main": "dist/emain.js",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
+22
-56
@@ -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;
|
||||
|
||||
@@ -44,16 +34,18 @@
|
||||
--floating-logo-width: 40px;
|
||||
--floating-logo-height: var(--screentabs-height);
|
||||
|
||||
/* right sidebar triggers */
|
||||
--floating-right-sidebar-triggers-width-darwin: 50px;
|
||||
--floating-right-sidebar-triggers-width: 40px;
|
||||
|
||||
/* 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);
|
||||
@@ -62,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);
|
||||
@@ -112,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);
|
||||
@@ -152,7 +125,6 @@
|
||||
--line-text-color: rgb(211, 215, 207);
|
||||
--line-svg-fill-color: rgb(150, 152, 150);
|
||||
--line-svg-hover-fill-color: #eceeec;
|
||||
--line-selected-border-color: rgb(193, 195, 193);
|
||||
--line-separator-color: rgb(126, 126, 126);
|
||||
--line-error-color: var(--app-error-color);
|
||||
--line-warning-color: var(--app-warning-color);
|
||||
@@ -175,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);
|
||||
@@ -196,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);
|
||||
@@ -219,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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+27
-4
@@ -101,6 +101,7 @@ body input.input {
|
||||
font-size: 10px;
|
||||
font-weight: 300;
|
||||
line-height: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.text-s1.icon {
|
||||
@@ -302,6 +303,25 @@ a.a-block {
|
||||
}
|
||||
}
|
||||
|
||||
.right-sidebar-triggers {
|
||||
position: absolute;
|
||||
z-index: 25;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
height: 38px;
|
||||
padding: 0 5px;
|
||||
|
||||
.right-sidebar-trigger {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-app-region: no-drag;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.copied-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -716,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,6 +820,7 @@ a.a-block {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
.info-message {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -848,6 +869,7 @@ a.a-block {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-color-icon,
|
||||
@@ -861,6 +883,7 @@ a.a-block {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
min-width: 70px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-color-select,
|
||||
|
||||
+35
-11
@@ -16,10 +16,12 @@ import { BookmarksView } from "./bookmarks/bookmarks";
|
||||
import { HistoryView } from "./history/history";
|
||||
import { ConnectionsView } from "./connections/connections";
|
||||
import { ClientSettingsView } from "./clientsettings/clientsettings";
|
||||
import { MainSideBar } from "./sidebar/sidebar";
|
||||
import { DisconnectedModal, ClientStopModal } from "./common/modals";
|
||||
import { ModalsProvider } from "./common/modals/provider";
|
||||
import { ErrorBoundary } from "./common/error/errorboundary";
|
||||
import { MainSideBar } from "./sidebar/main";
|
||||
import { RightSideBar } from "./sidebar/right";
|
||||
import { DisconnectedModal, ClientStopModal } from "@/modals";
|
||||
import { ModalsProvider } from "@/modals/provider";
|
||||
import { Button } from "@/elements";
|
||||
import { ErrorBoundary } from "@/common/error/errorboundary";
|
||||
import cn from "classnames";
|
||||
import "./app.less";
|
||||
|
||||
@@ -65,9 +67,17 @@ class App extends React.Component<{}, {}> {
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
openSidebar() {
|
||||
const width = GlobalModel.mainSidebarModel.getWidth(true);
|
||||
GlobalCommandRunner.clientSetSidebar(width, false);
|
||||
openMainSidebar() {
|
||||
const mainSidebarModel = GlobalModel.mainSidebarModel;
|
||||
const width = mainSidebarModel.getWidth(true);
|
||||
mainSidebarModel.saveState(width, false);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
openRightSidebar() {
|
||||
const rightSidebarModel = GlobalModel.rightSidebarModel;
|
||||
const width = rightSidebarModel.getWidth(true);
|
||||
rightSidebarModel.saveState(width, false);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -110,23 +120,36 @@ class App extends React.Component<{}, {}> {
|
||||
}
|
||||
// used to force a full reload of the application
|
||||
const renderVersion = GlobalModel.renderVersion.get();
|
||||
const sidebarCollapsed = GlobalModel.mainSidebarModel.getCollapsed();
|
||||
const mainSidebarCollapsed = GlobalModel.mainSidebarModel.getCollapsed();
|
||||
const rightSidebarCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
|
||||
const activeMainView = GlobalModel.activeMainView.get();
|
||||
const lightDarkClass = GlobalModel.isThemeDark() ? "is-dark" : "is-light";
|
||||
return (
|
||||
<div
|
||||
key={"version-" + renderVersion}
|
||||
id="main"
|
||||
className={cn("platform-" + platform, { "sidebar-collapsed": sidebarCollapsed }, lightDarkClass)}
|
||||
className={cn(
|
||||
"platform-" + platform,
|
||||
{ "mainsidebar-collapsed": mainSidebarCollapsed, "rightsidebar-collapsed": rightSidebarCollapsed },
|
||||
lightDarkClass
|
||||
)}
|
||||
onContextMenu={this.handleContextMenu}
|
||||
>
|
||||
<If condition={sidebarCollapsed}>
|
||||
<If condition={mainSidebarCollapsed}>
|
||||
<div key="logo-button" className="logo-button-container">
|
||||
<div className="logo-button-spacer" />
|
||||
<div className="logo-button" onClick={this.openSidebar}>
|
||||
<div className="logo-button" onClick={this.openMainSidebar}>
|
||||
<img src="public/logos/wave-logo.png" alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
<If condition={GlobalModel.isDev && rightSidebarCollapsed && activeMainView == "session"}>
|
||||
<div className="right-sidebar-triggers">
|
||||
<Button className="secondary ghost right-sidebar-trigger" onClick={this.openRightSidebar}>
|
||||
<i className="fa-sharp fa-regular fa-lightbulb"></i>
|
||||
</Button>
|
||||
</div>
|
||||
</If>
|
||||
<div ref={this.mainContentRef} className="main-content">
|
||||
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
|
||||
<ErrorBoundary>
|
||||
@@ -137,6 +160,7 @@ class App extends React.Component<{}, {}> {
|
||||
<ConnectionsView model={remotesModel} />
|
||||
<ClientSettingsView model={remotesModel} />
|
||||
</ErrorBoundary>
|
||||
<RightSideBar parentRef={this.mainContentRef} clientData={clientData} />
|
||||
</div>
|
||||
<ModalsProvider />
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,59 @@ type BookmarkProps = {
|
||||
bookmark: BookmarkType;
|
||||
};
|
||||
|
||||
class BookmarkKeybindings extends React.Component<{}, {}> {
|
||||
@boundMethod
|
||||
componentDidMount(): void {
|
||||
let keybindManager = GlobalModel.keybindManager;
|
||||
let bookmarksModel = GlobalModel.bookmarksModel;
|
||||
keybindManager.registerKeybinding("mainview", "bookmarks", "generic:cancel", (waveEvent) => {
|
||||
bookmarksModel.handleUserClose();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("mainview", "bookmarks", "generic:deleteItem", (waveEvent) => {
|
||||
bookmarksModel.handleUserDelete();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("mainview", "bookmarks", "generic:selectAbove", (waveEvent) => {
|
||||
bookmarksModel.handleUserNavigate(-1);
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("mainview", "bookmarks", "generic:selectBelow", (waveEvent) => {
|
||||
bookmarksModel.handleUserNavigate(1);
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("mainview", "bookmarks", "generic:selectPageAbove", (waveEvent) => {
|
||||
bookmarksModel.handleUserNavigate(-10);
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("mainview", "bookmarks", "generic:selectPageBelow", (waveEvent) => {
|
||||
bookmarksModel.handleUserNavigate(10);
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("mainview", "bookmarks", "generic:confirm", (waveEvent) => {
|
||||
bookmarksModel.handleUserConfirm();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("mainview", "bookmarks", "bookmarks:edit", (waveEvent) => {
|
||||
bookmarksModel.handleUserEdit();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("mainview", "bookmarks", "bookmarks:copy", (waveEvent) => {
|
||||
bookmarksModel.handleUserCopy();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
componentWillUnmount() {
|
||||
GlobalModel.keybindManager.unregisterDomain("bookmarks");
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class Bookmark extends React.Component<BookmarkProps, {}> {
|
||||
@boundMethod
|
||||
@@ -194,6 +247,9 @@ class BookmarksView extends React.Component<{}, {}> {
|
||||
let bookmark: BookmarkType = null;
|
||||
return (
|
||||
<MainView className="bookmarks-view" title="Bookmarks" onClose={this.handleClose}>
|
||||
<If condition={!isHidden}>
|
||||
<BookmarkKeybindings></BookmarkKeybindings>
|
||||
</If>
|
||||
<div className="bookmarks-list">
|
||||
<For index="idx" each="bookmark" of={bookmarks}>
|
||||
<Bookmark key={bookmark.bookmarkid} bookmark={bookmark} />
|
||||
|
||||
@@ -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=""
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
.wave-button {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
|
||||
display: flex;
|
||||
padding: 6px 16px;
|
||||
align-items: center;
|
||||
@@ -13,57 +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 {
|
||||
border: 1px solid var(--form-element-primary-color);
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
// Styles for .ghost are already defined above
|
||||
}
|
||||
|
||||
&: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: var(--form-element-secondary-color);
|
||||
i {
|
||||
fill: var(--form-element-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary.outlined {
|
||||
background: none;
|
||||
border: 1px solid var(--form-element-text-color);
|
||||
}
|
||||
|
||||
&.solid {
|
||||
color: var(--form-element-text-color);
|
||||
background: var(--form-element-secondary-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
&.secondary.ghost {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
border: 1px solid var(--form-element-secondary-color);
|
||||
}
|
||||
&.secondary.danger {
|
||||
color: var(--app-error-color);
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
padding: 6px 10px;
|
||||
|
||||
i {
|
||||
fill: var(--form-element-primary-color);
|
||||
}
|
||||
}
|
||||
&.small {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 3.6px;
|
||||
}
|
||||
|
||||
&.term-inline {
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./button.less";
|
||||
|
||||
type ButtonVariantType = "outlined" | "solid" | "ghost";
|
||||
type ButtonThemeType = "primary" | "secondary";
|
||||
|
||||
interface ButtonProps {
|
||||
theme?: ButtonThemeType;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
variant?: ButtonVariantType;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
color?: string;
|
||||
style?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
className?: string;
|
||||
@@ -27,10 +18,8 @@ interface ButtonProps {
|
||||
|
||||
class Button extends React.Component<ButtonProps> {
|
||||
static defaultProps = {
|
||||
theme: "primary",
|
||||
variant: "solid",
|
||||
color: "",
|
||||
style: {},
|
||||
className: "primary",
|
||||
};
|
||||
|
||||
@boundMethod
|
||||
@@ -41,31 +30,11 @@ class Button extends React.Component<ButtonProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
theme,
|
||||
children,
|
||||
disabled,
|
||||
variant,
|
||||
color,
|
||||
style,
|
||||
autoFocus,
|
||||
termInline,
|
||||
className,
|
||||
} = this.props;
|
||||
const { leftIcon, rightIcon, children, disabled, style, autoFocus, termInline, className } = this.props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"wave-button",
|
||||
theme,
|
||||
variant,
|
||||
color,
|
||||
{ disabled: disabled },
|
||||
{ "term-inline": termInline },
|
||||
className
|
||||
)}
|
||||
className={cn("wave-button", { disabled }, { "term-inline": termInline }, className)}
|
||||
onClick={this.handleClick}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)}>
|
||||
↑
|
||||
</Button>
|
||||
<Button className="secondary ghost" onClick={() => changeMonth(1)}>
|
||||
↓
|
||||
</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 };
|
||||
@@ -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,21 +0,0 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as React from "react";
|
||||
import { Button } from "./button";
|
||||
class IconButton extends Button {
|
||||
render() {
|
||||
const { children, theme, variant = "solid", ...rest } = this.props;
|
||||
const className = `wave-button icon-button ${theme} ${variant}`;
|
||||
|
||||
return (
|
||||
<button {...rest} className={className}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default IconButton;
|
||||
|
||||
export { IconButton };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user