Zoom, better sizes, color picker, sidebar footer

This commit is contained in:
Gregory Schier
2023-03-08 19:22:04 -08:00
parent 2bb2061f97
commit 2434f373be
26 changed files with 424 additions and 239 deletions

131
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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();

View File

@@ -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")]

View File

@@ -33,8 +33,8 @@ function App() {
<div className="grid grid-rows-[auto_1fr] h-full overflow-hidden">
<HStack
as={WindowDragRegion}
className="px-3 bg-background text-sm text-gray-900 border-b border-b-gray-200 pt-[1px]"
items="center"
className="px-3 bg-background text-gray-900 border-b border-b-gray-200 pt-[1px]"
alignItems="center"
>
{request.name}
</HStack>

View File

@@ -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<T extends ElementType> = ButtonHTMLAttributes<HTMLButtonElement> & {
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
color?: keyof typeof colorStyles;
size?: 'xs' | 'sm' | 'md';
size?: 'sm' | 'md';
justify?: 'start' | 'center';
forDropdown?: boolean;
as?: T;
};
export const Button = forwardRef(function Button<T extends ElementType>(
export const Button = forwardRef(function Button(
{
className,
as,
justify = 'center',
children,
size = 'md',
forDropdown,
color,
justify = 'center',
size = 'md',
type = 'button',
...props
}: ButtonProps<T> & Omit<ComponentPropsWithoutRef<T>, keyof ButtonProps<T>>,
}: ButtonProps,
ref: ForwardedRef<HTMLButtonElement>,
) {
const Component = as || 'button';
return (
<Component
<button
ref={ref}
type="button"
type={type}
className={classnames(
className,
'outline-none',
'border border-transparent focus-visible:border-blue-300',
'transition-all rounded-md flex items-center',
'bg-opacity-90 hover:bg-opacity-100',
colorStyles[color || 'default'],
justify === 'start' && 'justify-start',
justify === 'center' && 'justify-center',
size === 'md' && 'h-10 px-4',
size === 'sm' && 'h-8 px-3 text-sm',
size === 'xs' && 'h-7 px-2.5 text-sm',
size === 'md' && 'h-9 px-3',
size === 'sm' && 'h-7 px-2.5 text-sm',
)}
{...props}
>
{children}
{forDropdown && <Icon icon="triangleDown" className="ml-1 -mr-1" />}
</Component>
</button>
);
});

View File

@@ -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<typeof Link> & LinkProps;
type Props = ButtonProps & LinkProps;
export function ButtonLink({ ...props }: Props) {
return <Button as={Link} {...props} />;
export function ButtonLink({
reloadDocument,
replace,
state,
preventScrollReset,
relative,
to,
className,
...buttonProps
}: Props) {
const linkProps = { reloadDocument, replace, state, preventScrollReset, relative, to };
return (
<Link {...linkProps}>
<Button className={classnames(className, 'w-full')} {...buttonProps} />
</Link>
);
}

View File

@@ -43,7 +43,7 @@ export function Dialog({
<IconButton aria-label="Close" icon="x" size="sm" />
</D.Close>
<VStack space={3}>
<HStack items="center" className="pb-3">
<HStack alignItems="center" className="pb-3">
<D.Title className="text-xl font-semibold">{title}</D.Title>
</HStack>
{description && <D.Description>{description}</D.Description>}

View File

@@ -25,19 +25,15 @@
}
.cm-gutters {
@apply border-0 text-gray-500 text-opacity-30;
@apply border-0 text-gray-500/60;
.cm-gutterElement {
@apply cursor-default;
}
}
&.cm-focused .cm-gutters {
@apply text-opacity-60;
}
.placeholder-widget {
@apply text-[0.9em] text-gray-800 dark:text-gray-1000 px-1 rounded cursor-default dark:shadow;
@apply text-[0.9em] text-gray-800 dark:text-gray-900 px-1 rounded cursor-default dark:shadow;
/* NOTE: Background and border are translucent so we can see text selection through it */
@apply bg-gray-300/40 border border-gray-300 border-opacity-40 hover:border-opacity-80;
@@ -105,11 +101,12 @@
}
.cm-editor .fold-gutter-icon:hover {
@apply text-gray-400 bg-gray-100/20;
@apply text-gray-900 bg-gray-300/50;
}
.cm-editor .cm-foldPlaceholder {
@apply px-2 border border-gray-200 bg-gray-100;
@apply px-2 border border-gray-400/50 bg-gray-300/50 cursor-default;
@apply hover:text-gray-800 hover:border-gray-400;
}
.cm-editor .cm-activeLineGutter,

View File

@@ -34,7 +34,6 @@ import {
} from '@codemirror/view';
import { tags as t } from '@lezer/highlight';
import { debouncedAutocompletionDisplay } from './autocomplete';
import { readOnlyTransactionFilter } from './readOnlyTransactionFilter';
import { twig } from './twig/extension';
import { url } from './url/extension';

View File

@@ -106,7 +106,7 @@ function FormRow({
defaultValue={value}
onChange={onChangeValue}
/>
{onDelete && <IconButton size="sm" icon="trash" onClick={onDelete} />}
{onDelete && <IconButton icon="trash" onClick={onDelete} />}
</HStack>
{addSubmit && <input type="submit" value="Add" className="sr-only" />}
</div>

View File

@@ -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<IconName, NamedExoticComponent<{ className: string }>> = {
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',
)}
/>

View File

@@ -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<IconProps, 'size'> &
ButtonProps<typeof Button> & {
iconClassName?: string;
};
type Props = IconProps & ButtonProps & { iconClassName?: string; iconSize?: IconProps['size'] };
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
{ icon, spin, className, iconClassName, ...props }: Props,
{ icon, spin, className, iconClassName, size, iconSize, ...props }: Props,
ref,
) {
return (
<Button ref={ref} className={classnames(className, 'group')} {...props}>
<Button ref={ref} className={classnames(className, 'group')} size={size} {...props}>
<Icon
size={iconSize}
icon={icon}
spin={spin}
className={classnames(

View File

@@ -59,13 +59,13 @@ export function Input({
{label}
</label>
<HStack
items="center"
alignItems="center"
className={classnames(
containerClassName,
'relative w-full rounded-md text-gray-900',
'border border-gray-200 focus-within:border-blue-400/40',
size === 'md' && 'h-10',
size === 'sm' && 'h-8',
size === 'md' && 'h-9',
size === 'sm' && 'h-7',
)}
>
{leftSlot}

View File

@@ -38,9 +38,9 @@ export function RequestPane({ fullHeight, request, className }: Props) {
{['JSON', 'Params', 'Headers', 'Auth'].map((label, i) => (
<Button
key={label}
size="xs"
color={i === 0 && 'gray'}
className={i !== 0 && 'opacity-80 hover:opacity-100'}
size="sm"
color={i === 0 ? 'gray' : undefined}
className={i !== 0 ? 'opacity-80 hover:opacity-100' : undefined}
>
{label}
</Button>

View File

@@ -61,7 +61,7 @@ export function ResponsePane({ requestId, className }: Props) {
{response && (
<>
<HStack
items="center"
alignItems="center"
className="italic text-gray-600 text-sm w-full mb-1 flex-shrink-0 pl-2"
>
{response.status > 0 && (
@@ -76,7 +76,7 @@ export function ResponsePane({ requestId, className }: Props) {
</div>
)}
<HStack items="center" className="ml-auto h-8">
<HStack alignItems="center" className="ml-auto h-8">
{contentType.includes('html') && (
<IconButton
icon={viewMode === 'pretty' ? 'eye' : 'code'}

View File

@@ -1,11 +1,12 @@
import classnames from 'classnames';
import type { HTMLAttributes } from 'react';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { SketchPicker } from 'react-color';
import { useRequestCreate } from '../hooks/useRequest';
import useTheme from '../hooks/useTheme';
import type { HttpRequest } from '../lib/models';
import { Button } from './Button';
import { ButtonLink } from './ButtonLink';
import { Dialog } from './Dialog';
import { HeaderEditor } from './HeaderEditor';
import { IconButton } from './IconButton';
@@ -20,28 +21,27 @@ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, '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<boolean>(false);
const [color, setColor] = useState<string>('blue');
const [showPicker, setShowPicker] = useState<boolean>(false);
return (
<div
className={classnames(className, 'w-52 bg-gray-100 h-full border-r border-gray-200')}
className={classnames(
className,
'min-w-[10rem] bg-gray-100 h-full border-r border-gray-200 relative',
)}
{...props}
>
<HStack as={WindowDragRegion} items="center" justify="end">
<HStack as={WindowDragRegion} alignItems="center" justifyContent="end">
<Dialog wide open={open} onOpenChange={setOpen} title="Edit Headers">
<HeaderEditor />
<Button className="ml-auto mt-5" color="primary" onClick={() => setOpen(false)}>
Save
</Button>
</Dialog>
<IconButton size="sm" icon="camera" onClick={() => setOpen(true)} />
<IconButton
size="sm"
icon={appearance === 'dark' ? 'moon' : 'sun'}
onClick={toggleAppearance}
/>
<IconButton
size="sm"
className="mx-1"
icon="plusCircle"
onClick={async () => {
await createRequest.mutate({ name: 'Test Request' });
@@ -53,6 +53,27 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
<SidebarItem key={r.id} request={r} active={r.id === activeRequestId} />
))}
{/*<Colors />*/}
<HStack
className="absolute bottom-1 left-1 right-0 mx-1"
alignItems="center"
justifyContent="end"
>
<IconButton icon="colorWheel" onClick={() => setShowPicker((p) => !p)} />
<IconButton icon={appearance === 'dark' ? 'moon' : 'sun'} onClick={toggleAppearance} />
<IconButton icon="rows" onClick={() => setOpen(true)} />
</HStack>
{showPicker && (
<SketchPicker
className="fixed z-10 bottom-2 right-2"
color={color}
onChange={(c) => {
setColor(c.hex);
forceSetTheme(c.hex);
}}
/>
)}
</VStack>
</div>
);
@@ -61,15 +82,16 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
function SidebarItem({ request, active }: { request: HttpRequest; active: boolean }) {
return (
<li key={request.id}>
<Button
as={Link}
<ButtonLink
color="custom"
to={`/workspaces/${request.workspaceId}/requests/${request.id}`}
className={classnames('w-full', active ? 'bg-gray-200/70 text-gray-900' : 'text-gray-500')}
size="xs"
disabled={active}
className={classnames('w-full', active ? 'bg-gray-200/70 text-gray-900' : 'text-gray-600')}
size="sm"
justify="start"
>
{request.name || request.url}
</Button>
</ButtonLink>
</li>
);
}

View File

@@ -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<HTMLElement> {
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 (
<Component
className={classnames(
className,
'flex flex-grow-0',
items === 'center' && 'items-center',
items === 'start' && 'items-start',
justify === 'start' && 'justify-start',
justify === 'center' && 'justify-center',
justify === 'end' && 'justify-end',
alignItems === 'center' && 'items-center',
alignItems === 'start' && 'items-start',
justifyContent === 'start' && 'justify-start',
justifyContent === 'center' && 'justify-center',
justifyContent === 'end' && 'justify-end',
)}
{...props}
/>
>
{children}
</Component>
);
}

View File

@@ -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
<Input
hideLabel
useEditor={{ useTemplating: true, contentType: 'url' }}
size="sm"
className="font-mono px-0"
name="url"
label="Enter URL"
@@ -46,7 +45,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
{ label: 'HEAD', value: 'HEAD' },
]}
>
<Button type="button" disabled={loading} size="xs" className="mx-0.5" justify="start">
<Button type="button" disabled={loading} size="sm" className="mx-0.5" justify="start">
{method.toUpperCase()}
</Button>
</DropdownMenuRadio>
@@ -55,7 +54,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
<IconButton
type="submit"
className="mr-0.5"
size="xs"
size="sm"
icon={loading ? 'update' : 'paperPlane'}
spin={loading}
disabled={loading}

View File

@@ -7,7 +7,7 @@ export function WindowDragRegion({ className, ...props }: Props) {
return (
<div
data-tauri-drag-region
className={classnames(className, 'w-full h-10 flex-shrink-0')}
className={classnames(className, 'w-full h-12 flex-shrink-0')}
{...props}
/>
);

View File

@@ -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,
};
}

1
src-web/lib/constants.ts Normal file
View File

@@ -0,0 +1 @@
export const DEFAULT_FONT_SIZE = 16;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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: '/',

View File

@@ -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% / <alpha-value>)',
black: 'hsl(0 100% 0% / <alpha-value>)',
background: 'hsl(var(--color-background) / <alpha-value>)',
placeholder: 'hsl(var(--color-gray-300) / <alpha-value>)',
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% / <alpha-value>)",
black: "hsl(0 100% 0% / <alpha-value>)",
background: "hsl(var(--color-background) / <alpha-value>)",
placeholder: "hsl(var(--color-gray-400) / <alpha-value>)",
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) / <alpha-value>)`,
50: `hsl(var(--color-${name}-50) / <alpha-value>)`,
100: `hsl(var(--color-${name}-100) / <alpha-value>)`,
200: `hsl(var(--color-${name}-200) / <alpha-value>)`,
300: `hsl(var(--color-${name}-300) / <alpha-value>)`,
400: `hsl(var(--color-${name}-400) / <alpha-value>)`,
500: `hsl(var(--color-${name}-500) / <alpha-value>)`,
600: `hsl(var(--color-${name}-600) / <alpha-value>)`,
700: `hsl(var(--color-${name}-700) / <alpha-value>)`,
800: `hsl(var(--color-${name}-800) / <alpha-value>)`,
900: `hsl(var(--color-${name}-900) / <alpha-value>)`,
950: `hsl(var(--color-${name}-950) / <alpha-value>)`,
1000: `hsl(var(--color-${name}-1000) / <alpha-value>)`,
};
return {
0: `hsl(var(--color-${name}-0) / <alpha-value>)`,
50: `hsl(var(--color-${name}-50) / <alpha-value>)`,
100: `hsl(var(--color-${name}-100) / <alpha-value>)`,
200: `hsl(var(--color-${name}-200) / <alpha-value>)`,
300: `hsl(var(--color-${name}-300) / <alpha-value>)`,
400: `hsl(var(--color-${name}-400) / <alpha-value>)`,
500: `hsl(var(--color-${name}-500) / <alpha-value>)`,
600: `hsl(var(--color-${name}-600) / <alpha-value>)`,
700: `hsl(var(--color-${name}-700) / <alpha-value>)`,
800: `hsl(var(--color-${name}-800) / <alpha-value>)`,
900: `hsl(var(--color-${name}-900) / <alpha-value>)`,
950: `hsl(var(--color-${name}-950) / <alpha-value>)`,
1000: `hsl(var(--color-${name}-1000) / <alpha-value>)`
};
}