From 6c8f4c943a7725046ee6f3bf0d09f85e4265d3c4 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 8 Mar 2023 19:22:04 -0800 Subject: [PATCH] Zoom, better sizes, color picker, sidebar footer --- package-lock.json | 131 ++++++++++++++++++++++++ package.json | 2 + src-tauri/src/main.rs | 31 ++++-- src-tauri/src/window_ext.rs | 2 +- src-web/App.tsx | 4 +- src-web/components/Button.tsx | 48 ++++----- src-web/components/ButtonLink.tsx | 21 +++- src-web/components/Dialog.tsx | 2 +- src-web/components/Editor/Editor.css | 13 +-- src-web/components/Editor/extensions.ts | 1 - src-web/components/HeaderEditor.tsx | 2 +- src-web/components/Icon.tsx | 61 +++++------ src-web/components/IconButton.tsx | 16 ++- src-web/components/Input.tsx | 6 +- src-web/components/RequestPane.tsx | 6 +- src-web/components/ResponsePane.tsx | 4 +- src-web/components/Sidebar.tsx | 54 +++++++--- src-web/components/Stacks.tsx | 35 ++++--- src-web/components/UrlBar.tsx | 11 +- src-web/components/WindowDragRegion.tsx | 2 +- src-web/hooks/useTheme.ts | 10 +- src-web/lib/constants.ts | 1 + src-web/lib/theme/window.ts | 10 +- src-web/main.css | 63 ++++-------- src-web/main.tsx | 16 +++ tailwind.config.cjs | 111 +++++++++++--------- 26 files changed, 424 insertions(+), 239 deletions(-) create mode 100644 src-web/lib/constants.ts diff --git a/package-lock.json b/package-lock.json index c6a5590c..83ceee4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "parse-color": "^1.0.0", "parse-json": "^6.0.2", "react": "^18.2.0", + "react-color": "^2.19.3", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", "react-router-dom": "^6.8.1" @@ -45,6 +46,7 @@ "@types/parse-color": "^1.0.1", "@types/parse-json": "^4.0.0", "@types/react": "^18.0.15", + "@types/react-color": "^3.0.6", "@types/react-dom": "^18.0.6", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", @@ -1049,6 +1051,14 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -2254,6 +2264,16 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-color": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.6.tgz", + "integrity": "sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==", + "dev": true, + "dependencies": { + "@types/react": "*", + "@types/reactcss": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.0.11", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", @@ -2263,6 +2283,15 @@ "@types/react": "*" } }, + "node_modules/@types/reactcss": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz", + "integrity": "sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -5309,6 +5338,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5356,6 +5390,11 @@ "node": ">=12" } }, + "node_modules/material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6061,6 +6100,23 @@ "node": ">=0.10.0" } }, + "node_modules/react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "dependencies": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -6220,6 +6276,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, + "node_modules/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "dependencies": { + "lodash": "^4.0.1" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -6782,6 +6846,11 @@ "integrity": "sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==", "dev": true }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, "node_modules/tinypool": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.1.tgz", @@ -8138,6 +8207,12 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "requires": {} + }, "@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -9001,6 +9076,16 @@ "csstype": "^3.0.2" } }, + "@types/react-color": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.6.tgz", + "integrity": "sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==", + "dev": true, + "requires": { + "@types/react": "*", + "@types/reactcss": "*" + } + }, "@types/react-dom": { "version": "18.0.11", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", @@ -9010,6 +9095,15 @@ "@types/react": "*" } }, + "@types/reactcss": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz", + "integrity": "sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -11225,6 +11319,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -11266,6 +11365,11 @@ "@jridgewell/sourcemap-codec": "^1.4.13" } }, + "material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -11739,6 +11843,20 @@ "loose-envify": "^1.1.0" } }, + "react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "requires": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + } + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -11845,6 +11963,14 @@ } } }, + "reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "requires": { + "lodash": "^4.0.1" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -12259,6 +12385,11 @@ "integrity": "sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==", "dev": true }, + "tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, "tinypool": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.1.tgz", diff --git a/package.json b/package.json index 871a0c84..2e630aac 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "parse-color": "^1.0.0", "parse-json": "^6.0.2", "react": "^18.2.0", + "react-color": "^2.19.3", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", "react-router-dom": "^6.8.1" @@ -52,6 +53,7 @@ "@types/parse-color": "^1.0.1", "@types/parse-json": "^4.0.0", "@types/react": "^18.0.15", + "@types/react-color": "^3.0.6", "@types/react-dom": "^18.0.6", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8fa7821d..7661d108 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,6 @@ #![cfg_attr( - all(not(debug_assertions), target_os = "windows"), - windows_subsystem = "windows" +all(not(debug_assertions), target_os = "windows"), +windows_subsystem = "windows" )] #[cfg(target_os = "macos")] @@ -18,7 +18,7 @@ use sqlx::sqlite::SqlitePoolOptions; use sqlx::types::Json; use sqlx::{Pool, Sqlite}; use tauri::regex::Regex; -use tauri::{AppHandle, State, Wry}; +use tauri::{AppHandle, Menu, MenuItem, State, Submenu, Wry}; use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent}; use tokio::sync::Mutex; @@ -224,8 +224,8 @@ async fn update_request( request.headers.0, pool, ) - .await - .expect("Failed to update request"); + .await + .expect("Failed to update request"); app_handle .emit_all("updated_request", updated_request) @@ -317,12 +317,19 @@ fn greet(name: &str) -> String { } fn main() { - // here `"quit".to_string()` defines the menu item id, and the second parameter is the menu item label. let quit = CustomMenuItem::new("quit".to_string(), "Quit"); let tray_menu = SystemTrayMenu::new().add_item(quit); let system_tray = SystemTray::new().with_menu(tray_menu); + let submenu = Submenu::new("View", Menu::new() + .add_item(CustomMenuItem::new("zoom_reset".to_string(), "Zoom to Actual Size").accelerator("CmdOrCtrl+0")) + .add_item(CustomMenuItem::new("zoom_in".to_string(), "Zoom In").accelerator("CmdOrCtrl+Plus")) + .add_item(CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-")), + ); + let menu = Menu::new().add_native_item(MenuItem::Quit).add_submenu(submenu); + tauri::Builder::default() + .menu(menu) .system_tray(system_tray) .setup(|app| { let win = app.get_window("main").unwrap(); @@ -360,9 +367,19 @@ fn main() { window.hide().unwrap(); } _ => {} - } + }; } }) + .on_menu_event(|event| { + match event.menu_item_id() { + "quit" => std::process::exit(0), + "close" => event.window().close().unwrap(), + "zoom_reset" => event.window().emit("zoom", 0).unwrap(), + "zoom_in" => event.window().emit("zoom", 1).unwrap(), + "zoom_out" => event.window().emit("zoom", -1).unwrap(), + _ => {} + }; + }) .on_window_event(|e| { let apply_offset = || { let win = e.window(); diff --git a/src-tauri/src/window_ext.rs b/src-tauri/src/window_ext.rs index 5c719184..3b74cd5f 100644 --- a/src-tauri/src/window_ext.rs +++ b/src-tauri/src/window_ext.rs @@ -1,7 +1,7 @@ use tauri::{Runtime, Window}; const TRAFFIC_LIGHT_OFFSET_X: f64 = 15.0; -const TRAFFIC_LIGHT_OFFSET_Y: f64 = 18.0; +const TRAFFIC_LIGHT_OFFSET_Y: f64 = 22.0; pub trait WindowExt { #[cfg(target_os = "macos")] diff --git a/src-web/App.tsx b/src-web/App.tsx index 3fad7c11..2904ae9d 100644 --- a/src-web/App.tsx +++ b/src-web/App.tsx @@ -33,8 +33,8 @@ function App() {
{request.name} diff --git a/src-web/components/Button.tsx b/src-web/components/Button.tsx index ee000e8a..8537e0fa 100644 --- a/src-web/components/Button.tsx +++ b/src-web/components/Button.tsx @@ -1,65 +1,57 @@ import classnames from 'classnames'; -import type { - ButtonHTMLAttributes, - ComponentPropsWithoutRef, - ElementType, - ForwardedRef, -} from 'react'; +import type { ButtonHTMLAttributes, ForwardedRef } from 'react'; import { forwardRef } from 'react'; import { Icon } from './Icon'; const colorStyles = { - default: 'hover:bg-gray-700/10 text-gray-700 hover:text-gray-1000', - gray: 'text-gray-800 bg-gray-100 hover:bg-gray-500/20 hover:text-gray-1000', - primary: 'bg-blue-400 text-white', - secondary: 'bg-violet-400 text-white', - warning: 'bg-orange-400 text-white', - danger: 'bg-red-400 text-white', + custom: '', + default: 'text-gray-700 enabled:hover:bg-gray-700/10 enabled:hover:text-gray-1000', + gray: 'text-gray-800 bg-gray-100 enabled:hover:bg-gray-500/20 enabled:hover:text-gray-1000', + primary: 'bg-blue-400 text-white hover:bg-blue-500', + secondary: 'bg-violet-400 text-white hover:bg-violet-500', + warning: 'bg-orange-400 text-white hover:bg-orange-500', + danger: 'bg-red-400 text-white hover:bg-red-500', }; -export type ButtonProps = ButtonHTMLAttributes & { +export type ButtonProps = ButtonHTMLAttributes & { color?: keyof typeof colorStyles; - size?: 'xs' | 'sm' | 'md'; + size?: 'sm' | 'md'; justify?: 'start' | 'center'; forDropdown?: boolean; - as?: T; }; -export const Button = forwardRef(function Button( +export const Button = forwardRef(function Button( { className, - as, - justify = 'center', children, - size = 'md', forDropdown, color, + justify = 'center', + size = 'md', + type = 'button', ...props - }: ButtonProps & Omit, keyof ButtonProps>, + }: ButtonProps, ref: ForwardedRef, ) { - const Component = as || 'button'; return ( - {children} {forDropdown && } - + ); }); diff --git a/src-web/components/ButtonLink.tsx b/src-web/components/ButtonLink.tsx index c1931ed8..a3b8b6f8 100644 --- a/src-web/components/ButtonLink.tsx +++ b/src-web/components/ButtonLink.tsx @@ -1,10 +1,25 @@ +import classnames from 'classnames'; import type { LinkProps } from 'react-router-dom'; import { Link } from 'react-router-dom'; import type { ButtonProps } from './Button'; import { Button } from './Button'; -type Props = ButtonProps & LinkProps; +type Props = ButtonProps & LinkProps; -export function ButtonLink({ ...props }: Props) { - return
diff --git a/src-web/components/Icon.tsx b/src-web/components/Icon.tsx index da0ae116..32a58196 100644 --- a/src-web/components/Icon.tsx +++ b/src-web/components/Icon.tsx @@ -4,11 +4,13 @@ import { CheckIcon, ClockIcon, CodeIcon, + ColorWheelIcon, Cross2Icon, EyeOpenIcon, GearIcon, HomeIcon, MoonIcon, + ListBulletIcon, PaperPlaneIcon, PlusCircledIcon, PlusIcon, @@ -19,59 +21,40 @@ import { TriangleLeftIcon, TriangleRightIcon, UpdateIcon, + RowsIcon, } from '@radix-ui/react-icons'; import classnames from 'classnames'; -import type { NamedExoticComponent } from 'react'; -type IconName = - | 'archive' - | 'home' - | 'camera' - | 'gear' - | 'eye' - | 'triangleDown' - | 'triangleLeft' - | 'triangleRight' - | 'paperPlane' - | 'update' - | 'question' - | 'check' - | 'plus' - | 'plusCircle' - | 'clock' - | 'sun' - | 'code' - | 'x' - | 'trash' - | 'moon'; - -const icons: Record> = { - paperPlane: PaperPlaneIcon, - triangleDown: TriangleDownIcon, - plus: PlusIcon, - plusCircle: PlusCircledIcon, - clock: ClockIcon, +const icons = { archive: ArchiveIcon, camera: CameraIcon, check: CheckIcon, - triangleLeft: TriangleLeftIcon, - triangleRight: TriangleRightIcon, + clock: ClockIcon, + code: CodeIcon, + colorWheel: ColorWheelIcon, + eye: EyeOpenIcon, gear: GearIcon, home: HomeIcon, - update: UpdateIcon, - sun: SunIcon, + listBullet: ListBulletIcon, moon: MoonIcon, - x: Cross2Icon, + paperPlane: PaperPlaneIcon, + plus: PlusIcon, + plusCircle: PlusCircledIcon, question: QuestionMarkIcon, - eye: EyeOpenIcon, - code: CodeIcon, + rows: RowsIcon, + sun: SunIcon, trash: TrashIcon, + triangleDown: TriangleDownIcon, + triangleLeft: TriangleLeftIcon, + triangleRight: TriangleRightIcon, + update: UpdateIcon, + x: Cross2Icon, }; export interface IconProps { - icon: IconName; + icon: keyof typeof icons; className?: string; - size?: 'md'; + size?: 'xs' | 'sm' | 'md'; spin?: boolean; } @@ -83,6 +66,8 @@ export function Icon({ icon, spin, size = 'md', className }: IconProps) { className, 'text-gray-800', size === 'md' && 'h-4 w-4', + size === 'sm' && 'h-3 w-3', + size === 'xs' && 'h-2 w-2', spin && 'animate-spin', )} /> diff --git a/src-web/components/IconButton.tsx b/src-web/components/IconButton.tsx index 9d83e6f0..bd5372e5 100644 --- a/src-web/components/IconButton.tsx +++ b/src-web/components/IconButton.tsx @@ -1,22 +1,20 @@ +import classnames from 'classnames'; import { forwardRef } from 'react'; -import type { IconProps } from './Icon'; -import { Icon } from './Icon'; import type { ButtonProps } from './Button'; import { Button } from './Button'; -import classnames from 'classnames'; +import type { IconProps } from './Icon'; +import { Icon } from './Icon'; -type Props = Omit & - ButtonProps & { - iconClassName?: string; - }; +type Props = IconProps & ButtonProps & { iconClassName?: string; iconSize?: IconProps['size'] }; export const IconButton = forwardRef(function IconButton( - { icon, spin, className, iconClassName, ...props }: Props, + { icon, spin, className, iconClassName, size, iconSize, ...props }: Props, ref, ) { return ( - diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index d2678c71..91258a74 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -61,7 +61,7 @@ export function ResponsePane({ requestId, className }: Props) { {response && ( <> {response.status > 0 && ( @@ -76,7 +76,7 @@ export function ResponsePane({ requestId, className }: Props) { )} - + {contentType.includes('html') && ( , 'children'> { export function Sidebar({ className, activeRequestId, workspaceId, requests, ...props }: Props) { const createRequest = useRequestCreate({ workspaceId, navigateAfter: true }); - const { appearance, toggleAppearance } = useTheme(); + const { appearance, toggleAppearance, forceSetTheme } = useTheme(); const [open, setOpen] = useState(false); + const [color, setColor] = useState('blue'); + const [showPicker, setShowPicker] = useState(false); return (
- + - setOpen(true)} /> - { await createRequest.mutate({ name: 'Test Request' }); @@ -53,6 +53,27 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ... ))} {/**/} + + + setShowPicker((p) => !p)} /> + + setOpen(true)} /> + + + {showPicker && ( + { + setColor(c.hex); + forceSetTheme(c.hex); + }} + /> + )}
); @@ -61,15 +82,16 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ... function SidebarItem({ request, active }: { request: HttpRequest; active: boolean }) { return (
  • - +
  • ); } diff --git a/src-web/components/Stacks.tsx b/src-web/components/Stacks.tsx index 8d691657..55e57cac 100644 --- a/src-web/components/Stacks.tsx +++ b/src-web/components/Stacks.tsx @@ -1,6 +1,6 @@ -import type { HTMLAttributes, ReactNode } from 'react'; -import React, { Children, Fragment } from 'react'; import classnames from 'classnames'; +import type { ReactNode } from 'react'; +import React, { Children, Fragment } from 'react'; const spaceClassesX = { 0: 'pr-0', @@ -78,26 +78,35 @@ export function VStack({ className, space, children, ...props }: VStackProps) { ); } -interface BaseStackProps extends HTMLAttributes { - items?: 'start' | 'center'; - justify?: 'start' | 'center' | 'end'; +interface BaseStackProps { as?: React.ElementType; + alignItems?: 'start' | 'center'; + justifyContent?: 'start' | 'center' | 'end'; + className?: string; + children?: ReactNode; } -function BaseStack({ className, items, justify, as = 'div', ...props }: BaseStackProps) { +function BaseStack({ + className, + alignItems, + justifyContent, + children, + as = 'div', +}: BaseStackProps) { const Component = as; return ( + > + {children} + ); } diff --git a/src-web/components/UrlBar.tsx b/src-web/components/UrlBar.tsx index 396d4ec3..966a985c 100644 --- a/src-web/components/UrlBar.tsx +++ b/src-web/components/UrlBar.tsx @@ -1,8 +1,8 @@ -import { DropdownMenuRadio } from './Dropdown'; -import { Button } from './Button'; -import { Input } from './Input'; import type { FormEvent } from 'react'; +import { Button } from './Button'; +import { DropdownMenuRadio } from './Dropdown'; import { IconButton } from './IconButton'; +import { Input } from './Input'; interface Props { sendRequest: () => void; @@ -24,7 +24,6 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan - @@ -55,7 +54,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan ); diff --git a/src-web/hooks/useTheme.ts b/src-web/hooks/useTheme.ts index ec683b2f..edef69ff 100644 --- a/src-web/hooks/useTheme.ts +++ b/src-web/hooks/useTheme.ts @@ -1,4 +1,5 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { debounce } from 'lodash'; import { useEffect } from 'react'; import type { Appearance } from '../lib/theme/window'; import { @@ -10,13 +11,17 @@ import { const appearanceQueryKey = ['theme', 'appearance']; +const forceSetTheme = debounce((gray: string) => { + setAppearance(getAppearance(), gray); +}, 200); + export default function useTheme() { const queryClient = useQueryClient(); const appearance = useQuery({ queryKey: appearanceQueryKey, queryFn: getAppearance, initialData: getAppearance(), - }); + }).data; const themeChange = (appearance: Appearance) => { setAppearance(appearance); @@ -32,7 +37,8 @@ export default function useTheme() { }, []); return { - appearance: appearance.data, + appearance, + forceSetTheme, toggleAppearance: handleToggleAppearance, }; } diff --git a/src-web/lib/constants.ts b/src-web/lib/constants.ts new file mode 100644 index 00000000..d4332249 --- /dev/null +++ b/src-web/lib/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_FONT_SIZE = 16; diff --git a/src-web/lib/theme/window.ts b/src-web/lib/theme/window.ts index 6059b728..1dcd9a5f 100644 --- a/src-web/lib/theme/window.ts +++ b/src-web/lib/theme/window.ts @@ -28,7 +28,7 @@ const lightTheme: AppTheme = { appearance: 'light', layers: { root: { - whitePoint: 0.98, + whitePoint: 0.95, colors: { gray: '#7f8fb0', red: '#da4545', @@ -59,9 +59,15 @@ export function toggleAppearance(): Appearance { return newAppearance; } -export function setAppearance(a?: Appearance) { +export function setAppearance(a?: Appearance, gray?: string) { const appearance = a ?? getPreferredAppearance(); const theme = appearance === 'dark' ? darkTheme : lightTheme; + + // Hack to update the gray color for a demo + if (theme.layers.root && gray) { + theme.layers.root.colors.gray = gray; + } + document.documentElement.setAttribute('data-appearance', appearance); document.documentElement.setAttribute('data-theme', theme.name); diff --git a/src-web/main.css b/src-web/main.css index 04367ef2..d77963fc 100644 --- a/src-web/main.css +++ b/src-web/main.css @@ -2,49 +2,30 @@ @tailwind components; @tailwind utilities; -:root { - color-scheme: light dark; - --transition-duration: 100ms ease-in-out; -} - -:not(input):not(textarea), -:not(input):not(textarea)::after, -:not(input):not(textarea)::before { - -webkit-user-select: none; - user-select: none; - cursor: default; -} - -html, body, #root { - width: 100%; - height: 100%; - overflow: hidden; - - /* Default colors */ - background-color: hsl(var(--color-background)); - color: hsl(var(--color-gray-900)); -} - -* { - transition: background-color var(--transition-duration), - border-color var(--transition-duration), - box-shadow var(--transition-duration); -} - -/*.hide-scrollbar {*/ -/* &::-webkit-scrollbar-corner,*/ -/* &::-webkit-scrollbar {*/ -/* @apply w-[5px] h-[5px];*/ -/* background-color: transparent; !* or add it to the track *!*/ -/* }*/ - -/* &::-webkit-scrollbar-thumb {*/ -/* @apply bg-gray-100 bg-opacity-20 rounded-full;*/ -/* }*/ -/*}*/ - @layer base { + html, body, #root { + @apply w-full h-full overflow-hidden bg-gray-50 text-gray-900; + } + + /* Setup default transitions for elements */ + * { + transition: background-color var(--transition-duration), + border-color var(--transition-duration), + box-shadow var(--transition-duration); + } + + /* Disable user selection to make it more "app-like" */ + :not(input):not(textarea), + :not(input):not(textarea)::after, + :not(input):not(textarea)::before { + -webkit-user-select: none; + user-select: none; + cursor: default; + } + :root { + color-scheme: light dark; + --transition-duration: 100ms ease-in-out; --color-white: 255 100% 100%; --color-black: 255 0% 0%; --color-background: var(--color-gray-50); diff --git a/src-web/main.tsx b/src-web/main.tsx index 81f1a0f6..d6c36bf9 100644 --- a/src-web/main.tsx +++ b/src-web/main.tsx @@ -11,6 +11,7 @@ import { Layout } from './components/Layout'; import { RouterError } from './components/RouterError'; import { requestsQueryKey } from './hooks/useRequest'; import { responsesQueryKey } from './hooks/useResponses'; +import { DEFAULT_FONT_SIZE } from './lib/constants'; import type { HttpRequest, HttpResponse } from './lib/models'; import { convertDates } from './lib/models'; import { setAppearance } from './lib/theme/window'; @@ -75,6 +76,21 @@ await listen('updated_response', ({ payload: response }: { payload: HttpResponse ); }); +await listen('zoom', ({ payload: zoomDelta }: { payload: number }) => { + const fontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize); + + let newFontSize; + if (zoomDelta === 0) { + newFontSize = DEFAULT_FONT_SIZE; + } else if (zoomDelta > 0) { + newFontSize = Math.min(fontSize * 1.1, DEFAULT_FONT_SIZE * 5); + } else if (zoomDelta < 0) { + newFontSize = Math.max(fontSize * 0.9, DEFAULT_FONT_SIZE * 0.4); + } + + document.documentElement.style.fontSize = `${newFontSize}px`; +}); + const router = createBrowserRouter([ { path: '/', diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 8d771fca..9dd0f725 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -1,56 +1,65 @@ -/** @type {import('tailwindcss').Config} */ +/** @type {import("tailwindcss").Config} */ module.exports = { - darkMode: ['class', '[data-appearance="dark"]'], - content: [ - "./index.html", - "./src-web/**/*.{html,tsx}", - ], - theme: { - extend: {}, - fontFamily: { - 'mono': ['JetBrains Mono', "Menlo", 'monospace'], - }, - borderRadius: { - none: '0px', - sm: 'var(--border-radius-sm)', - DEFAULT: 'var(--border-radius)', - md: 'var(--border-radius-md)', - lg: 'var(--border-radius-lg)', - full: '9999px', - }, - colors: { - transparent: 'transparent', - white: 'hsl(0 100% 100% / )', - black: 'hsl(0 100% 0% / )', - background: 'hsl(var(--color-background) / )', - placeholder: 'hsl(var(--color-gray-300) / )', - red: color('red'), - orange: color('orange'), - yellow: color('yellow'), - gray: color('gray'), - blue: color('blue'), - green: color('green'), - pink: color('pink'), - violet: color('violet'), - } + darkMode: ["class", "[data-appearance=\"dark\"]"], + content: [ + "./index.html", + "./src-web/**/*.{html,tsx}" + ], + theme: { + extend: {}, + fontFamily: { + "mono": ["JetBrains Mono", "Menlo", "monospace"] }, - plugins: [], -} + fontSize: { + sm: "0.9rem", + base: "1rem", + xl: "1.25rem", + "2xl": "1.563rem", + "3xl": "1.953rem", + "4xl": "2.441rem", + "5xl": "3.052rem" + }, + borderRadius: { + none: "0px", + sm: "var(--border-radius-sm)", + DEFAULT: "var(--border-radius)", + md: "var(--border-radius-md)", + lg: "var(--border-radius-lg)", + full: "9999px" + }, + colors: { + transparent: "transparent", + white: "hsl(0 100% 100% / )", + black: "hsl(0 100% 0% / )", + background: "hsl(var(--color-background) / )", + placeholder: "hsl(var(--color-gray-400) / )", + red: color("red"), + orange: color("orange"), + yellow: color("yellow"), + gray: color("gray"), + blue: color("blue"), + green: color("green"), + pink: color("pink"), + violet: color("violet") + } + }, + plugins: [] +}; function color(name) { - return { - 0: `hsl(var(--color-${name}-0) / )`, - 50: `hsl(var(--color-${name}-50) / )`, - 100: `hsl(var(--color-${name}-100) / )`, - 200: `hsl(var(--color-${name}-200) / )`, - 300: `hsl(var(--color-${name}-300) / )`, - 400: `hsl(var(--color-${name}-400) / )`, - 500: `hsl(var(--color-${name}-500) / )`, - 600: `hsl(var(--color-${name}-600) / )`, - 700: `hsl(var(--color-${name}-700) / )`, - 800: `hsl(var(--color-${name}-800) / )`, - 900: `hsl(var(--color-${name}-900) / )`, - 950: `hsl(var(--color-${name}-950) / )`, - 1000: `hsl(var(--color-${name}-1000) / )`, - }; + return { + 0: `hsl(var(--color-${name}-0) / )`, + 50: `hsl(var(--color-${name}-50) / )`, + 100: `hsl(var(--color-${name}-100) / )`, + 200: `hsl(var(--color-${name}-200) / )`, + 300: `hsl(var(--color-${name}-300) / )`, + 400: `hsl(var(--color-${name}-400) / )`, + 500: `hsl(var(--color-${name}-500) / )`, + 600: `hsl(var(--color-${name}-600) / )`, + 700: `hsl(var(--color-${name}-700) / )`, + 800: `hsl(var(--color-${name}-800) / )`, + 900: `hsl(var(--color-${name}-900) / )`, + 950: `hsl(var(--color-${name}-950) / )`, + 1000: `hsl(var(--color-${name}-1000) / )` + }; }