Custom font sizes and better zoom

This commit is contained in:
Gregory Schier
2024-05-29 12:10:01 -07:00
parent 7c71d8b751
commit fbc684140b
55 changed files with 487 additions and 217 deletions

View File

@@ -21,7 +21,7 @@
</style> </style>
</head> </head>
<body> <body class="text-base">
<div id="root"></div> <div id="root"></div>
<div id="cm-portal" class="cm-portal"></div> <div id="cm-portal" class="cm-portal"></div>
<div id="react-portal"></div> <div id="react-portal"></div>

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings SET (\n theme, appearance, theme_dark, theme_light, update_channel\n ) = (?, ?, ?, ?, ?) WHERE id = 'default';\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "8e88c7070a34a6e151da66f521deeafaea9a12e2aa68081daaf235d2003b513d"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance,\n theme_dark, theme_light, update_channel\n FROM settings\n WHERE id = 'default'\n ", "query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance,\n theme_dark, theme_light, update_channel,\n interface_font_size, interface_scale, editor_font_size, editor_soft_wrap\n FROM settings\n WHERE id = 'default'\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -47,6 +47,26 @@
"name": "update_channel", "name": "update_channel",
"ordinal": 8, "ordinal": 8,
"type_info": "Text" "type_info": "Text"
},
{
"name": "interface_font_size",
"ordinal": 9,
"type_info": "Int64"
},
{
"name": "interface_scale",
"ordinal": 10,
"type_info": "Int64"
},
{
"name": "editor_font_size",
"ordinal": 11,
"type_info": "Int64"
},
{
"name": "editor_soft_wrap",
"ordinal": 12,
"type_info": "Bool"
} }
], ],
"parameters": { "parameters": {
@@ -61,8 +81,12 @@
false, false,
false, false,
false, false,
false,
false,
false,
false,
false false
] ]
}, },
"hash": "cae02809532d086fbde12a246d12e3839ec8610b66e08315106dbdbf25d8699c" "hash": "ca3485d87b060cd77c4114d2af544adf18f6f15341d9d5db40865e92a80da4e2"
} }

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings SET (\n theme, appearance, theme_dark, theme_light, update_channel,\n interface_font_size, interface_scale, editor_font_size, editor_soft_wrap\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?) WHERE id = 'default';\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "efd8ba41ea909b18dd520c57c1d464c5ae057b720cbbedcaec1513d43535632c"
}

View File

@@ -39,6 +39,7 @@
} }
] ]
}, },
"webview:allow-set-webview-zoom",
"window:allow-close", "window:allow-close",
"window:allow-is-fullscreen", "window:allow-is-fullscreen",
"window:allow-maximize", "window:allow-maximize",

View File

@@ -1 +1 @@
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["os:allow-os-type","event:allow-emit","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","event:allow-listen","event:allow-unlisten","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"name":"protoc","sidecar":true}]},"window:allow-close","window:allow-is-fullscreen","window:allow-maximize","window:allow-minimize","window:allow-toggle-maximize","window:allow-set-decorations","window:allow-set-title","window:allow-start-dragging","window:allow-unmaximize","window:allow-theme","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}} {"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["os:allow-os-type","event:allow-emit","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","event:allow-listen","event:allow-unlisten","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"name":"protoc","sidecar":true}]},"webview:allow-set-webview-zoom","window:allow-close","window:allow-is-fullscreen","window:allow-maximize","window:allow-minimize","window:allow-toggle-maximize","window:allow-set-decorations","window:allow-set-title","window:allow-start-dragging","window:allow-unmaximize","window:allow-theme","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}

View File

@@ -0,0 +1,4 @@
ALTER TABLE settings ADD COLUMN interface_font_size INTEGER DEFAULT 15 NOT NULL;
ALTER TABLE settings ADD COLUMN interface_scale INTEGER DEFAULT 1 NOT NULL;
ALTER TABLE settings ADD COLUMN editor_font_size INTEGER DEFAULT 13 NOT NULL;
ALTER TABLE settings ADD COLUMN editor_soft_wrap BOOLEAN DEFAULT 1 NOT NULL;

View File

@@ -1793,9 +1793,9 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow {
match event.id().0.as_str() { match event.id().0.as_str() {
"quit" => exit(0), "quit" => exit(0),
"close" => w.close().unwrap(), "close" => w.close().unwrap(),
"zoom_reset" => w.emit("zoom", 0).unwrap(), "zoom_reset" => w.emit("zoom_reset", true).unwrap(),
"zoom_in" => w.emit("zoom", 1).unwrap(), "zoom_in" => w.emit("zoom_in", true).unwrap(),
"zoom_out" => w.emit("zoom", -1).unwrap(), "zoom_out" => w.emit("zoom_out", true).unwrap(),
"settings" => w.emit("settings", true).unwrap(), "settings" => w.emit("settings", true).unwrap(),
"duplicate_request" => w.emit("duplicate_request", true).unwrap(), "duplicate_request" => w.emit("duplicate_request", true).unwrap(),
"refresh" => win2.eval("location.reload()").unwrap(), "refresh" => win2.eval("location.reload()").unwrap(),

View File

@@ -55,6 +55,10 @@ pub struct Settings {
pub theme_dark: String, pub theme_dark: String,
pub theme_light: String, pub theme_light: String,
pub update_channel: String, pub update_channel: String,
pub interface_font_size: i64,
pub interface_scale: i64,
pub editor_font_size: i64,
pub editor_soft_wrap: bool,
} }
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
@@ -886,7 +890,8 @@ async fn get_settings(mgr: &impl Manager<Wry>) -> Result<Settings, sqlx::Error>
r#" r#"
SELECT SELECT
id, model, created_at, updated_at, theme, appearance, id, model, created_at, updated_at, theme, appearance,
theme_dark, theme_light, update_channel theme_dark, theme_light, update_channel,
interface_font_size, interface_scale, editor_font_size, editor_soft_wrap
FROM settings FROM settings
WHERE id = 'default' WHERE id = 'default'
"#, "#,
@@ -922,14 +927,19 @@ pub async fn update_settings(
sqlx::query!( sqlx::query!(
r#" r#"
UPDATE settings SET ( UPDATE settings SET (
theme, appearance, theme_dark, theme_light, update_channel theme, appearance, theme_dark, theme_light, update_channel,
) = (?, ?, ?, ?, ?) WHERE id = 'default'; interface_font_size, interface_scale, editor_font_size, editor_soft_wrap
) = (?, ?, ?, ?, ?, ?, ?, ?, ?) WHERE id = 'default';
"#, "#,
settings.theme, settings.theme,
settings.appearance, settings.appearance,
settings.theme_dark, settings.theme_dark,
settings.theme_light, settings.theme_light,
settings.update_channel settings.update_channel,
settings.interface_font_size,
settings.interface_scale,
settings.editor_font_size,
settings.editor_soft_wrap,
) )
.execute(&db) .execute(&db)
.await?; .await?;

View File

@@ -49,7 +49,7 @@ export function BinaryFileEditor({
<Button variant="border" color="secondary" size="sm" onClick={handleClick}> <Button variant="border" color="secondary" size="sm" onClick={handleClick}>
Choose File Choose File
</Button> </Button>
<div className="text-xs font-mono truncate rtl pr-3 text-fg"> <div className="text-sm font-mono truncate rtl pr-3 text-fg">
{/* Special character to insert ltr text in rtl element without making things wonky */} {/* Special character to insert ltr text in rtl element without making things wonky */}
&#x200E; &#x200E;
{filePath ?? 'Select File'} {filePath ?? 'Select File'}
@@ -57,7 +57,7 @@ export function BinaryFileEditor({
</HStack> </HStack>
{filePath != null && mimeType !== contentType && !ignoreContentType.value && ( {filePath != null && mimeType !== contentType && !ignoreContentType.value && (
<Banner className="mt-3 !py-5"> <Banner className="mt-3 !py-5">
<div className="text-sm mb-4 text-center"> <div className="mb-4 text-center">
<div>Set Content-Type header</div> <div>Set Content-Type header</div>
<InlineCode>{mimeType}</InlineCode> for current request? <InlineCode>{mimeType}</InlineCode> for current request?
</div> </div>
@@ -65,12 +65,12 @@ export function BinaryFileEditor({
<Button <Button
variant="solid" variant="solid"
color="secondary" color="secondary"
size="xs" size="sm"
onClick={() => onChangeContentType(mimeType)} onClick={() => onChangeContentType(mimeType)}
> >
Set Header Set Header
</Button> </Button>
<Button size="xs" variant="border" onClick={() => ignoreContentType.set(true)}> <Button size="sm" variant="border" onClick={() => ignoreContentType.set(true)}>
Ignore Ignore
</Button> </Button>
</HStack> </HStack>

View File

@@ -28,7 +28,7 @@ export const CookieDialog = function ({ cookieJarId }: Props) {
return ( return (
<div className="pb-2"> <div className="pb-2">
<table className="w-full text-xs mb-auto min-w-full max-w-full divide-y divide-background-highlight"> <table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-background-highlight">
<thead> <thead>
<tr> <tr>
<th className="py-2 text-left">Domain</th> <th className="py-2 text-left">Domain</th>

View File

@@ -1,5 +1,6 @@
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { getCurrent } from '@tauri-apps/api/webviewWindow'; import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { useEffect } from 'react';
import { useCommandPalette } from '../hooks/useCommandPalette'; import { useCommandPalette } from '../hooks/useCommandPalette';
import { cookieJarsQueryKey } from '../hooks/useCookieJars'; import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { foldersQueryKey } from '../hooks/useFolders'; import { foldersQueryKey } from '../hooks/useFolders';
@@ -7,6 +8,7 @@ import { useGlobalCommands } from '../hooks/useGlobalCommands';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections'; import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents'; import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests'; import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
import { useHotKey } from '../hooks/useHotKey';
import { httpRequestsQueryKey } from '../hooks/useHttpRequests'; import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
import { httpResponsesQueryKey } from '../hooks/useHttpResponses'; import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue'; import { keyValueQueryKey } from '../hooks/useKeyValue';
@@ -16,15 +18,15 @@ import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { settingsQueryKey } from '../hooks/useSettings'; import { settingsQueryKey, useSettings } from '../hooks/useSettings';
import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument'; import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument';
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle'; import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
import { useUpdateSettings } from '../hooks/useUpdateSettings';
import { workspacesQueryKey } from '../hooks/useWorkspaces'; import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { useZoom } from '../hooks/useZoom';
import type { Model } from '../lib/models'; import type { Model } from '../lib/models';
import { modelsEq } from '../lib/models'; import { modelsEq } from '../lib/models';
const DEFAULT_FONT_SIZE = 16;
export function GlobalHooks() { export function GlobalHooks() {
// Include here so they always update, even if no component references them // Include here so they always update, even if no component references them
useRecentWorkspaces(); useRecentWorkspaces();
@@ -125,26 +127,43 @@ export function GlobalHooks() {
} }
}); });
useListenToTauriEvent<number>( const settings = useSettings();
'zoom', useEffect(() => {
({ payload: zoomDelta }) => { if (settings == null) {
const fontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize); return;
}
let newFontSize; const { interfaceScale, interfaceFontSize, editorFontSize } = settings;
if (zoomDelta === 0) { getCurrent().setZoom(interfaceScale).catch(console.error);
newFontSize = DEFAULT_FONT_SIZE; document.documentElement.style.cssText = [
} else if (zoomDelta > 0) { `font-size: ${interfaceFontSize}px`,
newFontSize = Math.min(fontSize * 1.1, DEFAULT_FONT_SIZE * 5); `--editor-font-size: ${editorFontSize}px`,
} else if (zoomDelta < 0) { ].join('; ');
newFontSize = Math.max(fontSize * 0.9, DEFAULT_FONT_SIZE * 0.4); }, [settings]);
} const updateSettings = useUpdateSettings();
document.documentElement.style.fontSize = `${newFontSize}px`; // Handle Zoom. Note, Mac handles it in app menu, so need to also handle keyboard
}, // shortcuts for Windows/Linux
{ const zoom = useZoom();
target: { kind: 'WebviewWindow', label: getCurrent().label }, useHotKey('app.zoom_in', () => zoom.zoomIn);
}, useListenToTauriEvent('zoom_in', () => zoom.zoomIn);
); useHotKey('app.zoom_out', () => zoom.zoomOut);
useListenToTauriEvent('zoom_out', () => zoom.zoomOut);
useHotKey('app.zoom_out', () => zoom.zoomReset);
useListenToTauriEvent('zoom_out', () => zoom.zoomReset);
useHotKey('app.zoom_out', () => {
if (!settings) return;
updateSettings.mutate({
...settings,
interfaceScale: Math.max(0.4, settings.interfaceScale * 0.9),
});
});
useHotKey('app.zoom_reset', () => {
if (!settings) return;
updateSettings.mutate({ ...settings, interfaceScale: 1 });
});
return null; return null;
} }

View File

@@ -105,7 +105,7 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
Message {activeEvent.eventType === 'client_message' ? 'Sent' : 'Received'} Message {activeEvent.eventType === 'client_message' ? 'Sent' : 'Received'}
</div> </div>
{!showLarge && activeEvent.content.length > 1000 * 1000 ? ( {!showLarge && activeEvent.content.length > 1000 * 1000 ? (
<VStack space={2} className="text-sm italic text-fg-subtler"> <VStack space={2} className="italic text-fg-subtler">
Message previews larger than 1MB are hidden Message previews larger than 1MB are hidden
<div> <div>
<Button <Button
@@ -136,7 +136,7 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
{activeEvent.content} {activeEvent.content}
</div> </div>
{activeEvent.error && ( {activeEvent.error && (
<div className="select-text cursor-text text-xs font-mono py-1 text-fg-warning"> <div className="select-text cursor-text text-sm font-mono py-1 text-fg-warning">
{activeEvent.error} {activeEvent.error}
</div> </div>
)} )}
@@ -220,11 +220,11 @@ function EventRow({
: 'info' : 'info'
} }
/> />
<div className={classNames('w-full truncate text-2xs')}> <div className={classNames('w-full truncate text-xs')}>
{content.slice(0, 1000)} {content.slice(0, 1000)}
{error && <span className="text-fg-warning"> ({error})</span>} {error && <span className="text-fg-warning"> ({error})</span>}
</div> </div>
<div className={classNames('opacity-50 text-2xs')}> <div className={classNames('opacity-50 text-xs')}>
{format(createdAt + 'Z', 'HH:mm:ss.SSS')} {format(createdAt + 'Z', 'HH:mm:ss.SSS')}
</div> </div>
</button> </button>

View File

@@ -209,7 +209,7 @@ export function GrpcConnectionSetupPane({
rightSlot={<Icon className="text-fg-subtler" size="sm" icon="chevronDown" />} rightSlot={<Icon className="text-fg-subtler" size="sm" icon="chevronDown" />}
disabled={isStreaming || services == null} disabled={isStreaming || services == null}
className={classNames( className={classNames(
'font-mono text-xs min-w-[5rem] !ring-0', 'font-mono text-sm min-w-[5rem] !ring-0',
paneSize < 400 && 'flex-1', paneSize < 400 && 'flex-1',
)} )}
> >

View File

@@ -40,7 +40,6 @@ export function GrpcProtoSelection({ requestId }: Props) {
<HStack space={2} justifyContent="start" className="flex-row-reverse"> <HStack space={2} justifyContent="start" className="flex-row-reverse">
<Button <Button
color="primary" color="primary"
size="sm"
onClick={async () => { onClick={async () => {
const selected = await open({ const selected = await open({
title: 'Select Proto Files', title: 'Select Proto Files',
@@ -61,7 +60,6 @@ export function GrpcProtoSelection({ requestId }: Props) {
isLoading={grpc.reflect.isFetching} isLoading={grpc.reflect.isFetching}
disabled={grpc.reflect.isFetching} disabled={grpc.reflect.isFetching}
color="secondary" color="secondary"
size="sm"
onClick={() => grpc.reflect.refetch()} onClick={() => grpc.reflect.refetch()}
> >
Refresh Schema Refresh Schema
@@ -103,25 +101,24 @@ export function GrpcProtoSelection({ requestId }: Props) {
)} )}
{protoFiles.length > 0 && ( {protoFiles.length > 0 && (
<table className="w-full divide-y"> <table className="w-full divide-y divide-background-highlight">
<thead> <thead>
<tr> <tr>
<th className="text-fg-subtler"> <th className="text-fg-subtler">
<span className="font-mono text-sm">*.proto</span> Files <span className="font-mono">*.proto</span> Files
</th> </th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y"> <tbody className="divide-y divide-background-highlight">
{protoFiles.map((f, i) => ( {protoFiles.map((f, i) => (
<tr key={f + i} className="group"> <tr key={f + i} className="group">
<td className="pl-1 text-sm font-mono">{f.split('/').pop()}</td> <td className="pl-1 font-mono">{f.split('/').pop()}</td>
<td className="w-0 py-0.5"> <td className="w-0 py-0.5">
<IconButton <IconButton
title="Remove file" title="Remove file"
size="sm"
icon="trash" icon="trash"
className="ml-auto opacity-30 transition-opacity group-hover:opacity-100" className="ml-auto opacity-50 transition-opacity group-hover:opacity-100"
onClick={async () => { onClick={async () => {
await protoFilesKv.set(protoFiles.filter((p) => p !== f)); await protoFilesKv.set(protoFiles.filter((p) => p !== f));
}} }}

View File

@@ -1,10 +1,10 @@
import { hotkeyActions } from '../hooks/useHotKey'; import { hotkeyActions } from '../hooks/useHotKey';
import { HotKeyList } from './core/HotKeyList'; import { HotKeyList } from './core/HotKeyList';
export const KeyboardShortcutsDialog = () => { export function KeyboardShortcutsDialog() {
return ( return (
<div className="h-full w-full pb-2"> <div className="h-full w-full pb-2">
<HotKeyList hotkeys={hotkeyActions} /> <HotKeyList hotkeys={hotkeyActions} />
</div> </div>
); );
}; }

View File

@@ -45,7 +45,7 @@ export function RecentConnectionsDropdown({
label: ( label: (
<HStack space={2} alignItems="center"> <HStack space={2} alignItems="center">
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago &bull;{' '} {formatDistanceToNowStrict(c.createdAt + 'Z')} ago &bull;{' '}
<span className="font-mono text-xs">{c.elapsed}ms</span> <span className="font-mono text-sm">{c.elapsed}ms</span>
</HStack> </HStack>
), ),
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />, leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,

View File

@@ -86,7 +86,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
hotkeyAction="request_switcher.toggle" hotkeyAction="request_switcher.toggle"
className={classNames( className={classNames(
className, className,
'text-fg text-sm truncate pointer-events-auto', 'text-fg truncate pointer-events-auto',
activeRequest === null && 'text-fg-subtler italic', activeRequest === null && 'text-fg-subtler italic',
)} )}
> >

View File

@@ -43,13 +43,13 @@ export const RecentResponsesDropdown = function ResponsePane({
disabled: responses.length === 0, disabled: responses.length === 0,
}, },
{ type: 'separator', label: 'History' }, { type: 'separator', label: 'History' },
...responses.slice(0, 20).map((r) => ({ ...responses.slice(0, 20).map((r: HttpResponse) => ({
key: r.id, key: r.id,
label: ( label: (
<HStack space={2} alignItems="center"> <HStack space={2} alignItems="center">
<StatusTag className="text-xs" response={r} /> <StatusTag className="text-sm" response={r} />
<span>&rarr;</span>{' '} <span>&rarr;</span>{' '}
<span className="font-mono text-xs">{r.elapsed >= 0 ? `${r.elapsed}ms` : 'n/a'}</span> <span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : 'n/a'}</span>
</HStack> </HStack>
), ),
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />, leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />,

View File

@@ -94,7 +94,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
<HStack <HStack
alignItems="center" alignItems="center"
className={classNames( className={classNames(
'text-fg-subtle text-sm w-full flex-shrink-0', 'text-fg-subtle w-full flex-shrink-0',
// Remove a bit of space because the tabs have lots too // Remove a bit of space because the tabs have lots too
'-mb-1.5', '-mb-1.5',
)} )}

View File

@@ -7,6 +7,7 @@ import { useSettings } from '../../hooks/useSettings';
import { useThemes } from '../../hooks/useThemes'; import { useThemes } from '../../hooks/useThemes';
import { useUpdateSettings } from '../../hooks/useUpdateSettings'; import { useUpdateSettings } from '../../hooks/useUpdateSettings';
import { trackEvent } from '../../lib/analytics'; import { trackEvent } from '../../lib/analytics';
import { clamp } from '../../lib/clamp';
import { isThemeDark } from '../../lib/theme/window'; import { isThemeDark } from '../../lib/theme/window';
import type { ButtonProps } from '../core/Button'; import type { ButtonProps } from '../core/Button';
import { Button } from '../core/Button'; import { Button } from '../core/Button';
@@ -14,8 +15,10 @@ import { Editor } from '../core/Editor';
import type { IconProps } from '../core/Icon'; import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon'; import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton'; import { IconButton } from '../core/IconButton';
import { PlainInput } from '../core/PlainInput';
import type { SelectOption } from '../core/Select'; import type { SelectOption } from '../core/Select';
import { Select } from '../core/Select'; import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import { HStack, VStack } from '../core/Stacks'; import { HStack, VStack } from '../core/Stacks';
const buttonColors: ButtonProps['color'][] = [ const buttonColors: ButtonProps['color'][] = [
@@ -75,6 +78,41 @@ export function SettingsAppearance() {
return ( return (
<VStack space={2} className="mb-4"> <VStack space={2} className="mb-4">
<PlainInput
size="sm"
name="interfaceFontSize"
label="Font Size"
placeholder="16"
type="number"
labelPosition="left"
defaultValue={`${settings.interfaceFontSize}`}
validate={(value) => parseInt(value) >= 8 && parseInt(value) <= 30}
onChange={(v) =>
updateSettings.mutate({
...settings,
interfaceFontSize: clamp(parseInt(v) || 16, 8, 30),
})
}
/>
<PlainInput
size="sm"
name="editorFontSize"
label="Editor Font Size"
placeholder="14"
type="number"
labelPosition="left"
defaultValue={`${settings.editorFontSize}`}
validate={(value) => parseInt(value) >= 8 && parseInt(value) <= 30}
onChange={(v) =>
updateSettings.mutate({
...settings,
editorFontSize: clamp(parseInt(v) || 14, 8, 30),
})
}
/>
<Separator className="my-4" />
<Select <Select
name="appearance" name="appearance"
label="Appearance" label="Appearance"
@@ -91,37 +129,35 @@ export function SettingsAppearance() {
{ label: 'Dark', value: 'dark' }, { label: 'Dark', value: 'dark' },
]} ]}
/> />
<div className="grid grid-cols-2 gap-3"> <Select
<Select name="lightTheme"
name="lightTheme" label="Light Theme"
label={'Light Theme' + (appearance !== 'dark' ? ' (active)' : '')} labelPosition="left"
labelPosition="top" size="sm"
size="sm" value={activeTheme.light.id}
value={activeTheme.light.id} options={lightThemes}
options={lightThemes} onChange={async (themeLight) => {
onChange={async (themeLight) => { await updateSettings.mutateAsync({ ...settings, themeLight });
await updateSettings.mutateAsync({ ...settings, themeLight }); trackEvent('setting', 'update', { themeLight });
trackEvent('setting', 'update', { themeLight }); }}
}} />
/> <Select
<Select name="darkTheme"
name="darkTheme" label="Dark Theme"
label={'Dark Theme' + (appearance === 'dark' ? ' (active)' : '')} labelPosition="left"
labelPosition="top" size="sm"
size="sm" value={activeTheme.dark.id}
value={activeTheme.dark.id} options={darkThemes}
options={darkThemes} onChange={async (themeDark) => {
onChange={async (themeDark) => { await updateSettings.mutateAsync({ ...settings, themeDark });
await updateSettings.mutateAsync({ ...settings, themeDark }); trackEvent('setting', 'update', { themeDark });
trackEvent('setting', 'update', { themeDark }); }}
}} />
/>
</div>
<VStack <VStack
space={3} space={3}
className="mt-3 w-full bg-background p-3 border border-dashed border-background-highlight rounded overflow-x-auto" className="mt-3 w-full bg-background p-3 border border-dashed border-background-highlight rounded overflow-x-auto"
> >
<div className="text-sm text-fg font-bold"> <div className="text-fg font-bold">
Theme Preview <span className="text-fg-subtle">({appearance})</span> Theme Preview <span className="text-fg-subtle">({appearance})</span>
</div> </div>
<HStack space={1.5} alignItems="center" className="w-full"> <HStack space={1.5} alignItems="center" className="w-full">

View File

@@ -16,8 +16,7 @@ enum Tab {
} }
const tabs = [Tab.General, Tab.Appearance, Tab.Design]; const tabs = [Tab.General, Tab.Appearance, Tab.Design];
const useTabState = createGlobalState<string>(tabs[0]!);
const useTabState = createGlobalState<string>(Tab.Appearance);
export const SettingsDialog = () => { export const SettingsDialog = () => {
const [tab, setTab] = useTabState(); const [tab, setTab] = useTabState();

View File

@@ -1,3 +1,4 @@
import React from 'react';
import { useActiveWorkspace } from '../../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
import { useAppInfo } from '../../hooks/useAppInfo'; import { useAppInfo } from '../../hooks/useAppInfo';
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates'; import { useCheckForUpdates } from '../../hooks/useCheckForUpdates';
@@ -35,19 +36,13 @@ export function SettingsGeneral() {
labelPosition="left" labelPosition="left"
size="sm" size="sm"
value={settings.updateChannel} value={settings.updateChannel}
onChange={async (updateChannel) => { onChange={(updateChannel) => {
trackEvent('setting', 'update', { update_channel: updateChannel }); trackEvent('setting', 'update', { update_channel: updateChannel });
await updateSettings.mutateAsync({ ...settings, updateChannel }); updateSettings.mutate({ ...settings, updateChannel });
}} }}
options={[ options={[
{ { label: 'Release', value: 'stable' },
label: 'Release', { label: 'Early Bird (Beta)', value: 'beta' },
value: 'stable',
},
{
label: 'Early Bird (Beta)',
value: 'beta',
},
]} ]}
/> />
<IconButton <IconButton
@@ -59,12 +54,11 @@ export function SettingsGeneral() {
onClick={() => checkForUpdates.mutateAsync()} onClick={() => checkForUpdates.mutateAsync()}
/> />
</div> </div>
<Separator className="my-4" /> <Separator className="my-4" />
<Heading size={2}> <Heading size={2}>
Workspace{' '} Workspace{' '}
<div className="inline-block ml-1 bg-background-highlight px-2 py-0.5 text-sm rounded text-fg"> <div className="inline-block ml-1 bg-background-highlight px-2 py-0.5 rounded text-fg text-shrink">
{workspace.name} {workspace.name}
</div> </div>
</Heading> </Heading>
@@ -77,28 +71,28 @@ export function SettingsGeneral() {
labelPosition="left" labelPosition="left"
defaultValue={`${workspace.settingRequestTimeout}`} defaultValue={`${workspace.settingRequestTimeout}`}
validate={(value) => parseInt(value) >= 0} validate={(value) => parseInt(value) >= 0}
onChange={(v) => updateWorkspace.mutateAsync({ settingRequestTimeout: parseInt(v) || 0 })} onChange={(v) => updateWorkspace.mutate({ settingRequestTimeout: parseInt(v) || 0 })}
/> />
<Checkbox <Checkbox
checked={workspace.settingValidateCertificates} checked={workspace.settingValidateCertificates}
title="Validate TLS Certificates" title="Validate TLS Certificates"
onChange={async (settingValidateCertificates) => { onChange={(settingValidateCertificates) => {
trackEvent('workspace', 'update', { trackEvent('workspace', 'update', {
validate_certificates: JSON.stringify(settingValidateCertificates), validate_certificates: JSON.stringify(settingValidateCertificates),
}); });
await updateWorkspace.mutateAsync({ settingValidateCertificates }); updateWorkspace.mutate({ settingValidateCertificates });
}} }}
/> />
<Checkbox <Checkbox
checked={workspace.settingFollowRedirects} checked={workspace.settingFollowRedirects}
title="Follow Redirects" title="Follow Redirects"
onChange={async (settingFollowRedirects) => { onChange={(settingFollowRedirects) => {
trackEvent('workspace', 'update', { trackEvent('workspace', 'update', {
follow_redirects: JSON.stringify(settingFollowRedirects), follow_redirects: JSON.stringify(settingFollowRedirects),
}); });
await updateWorkspace.mutateAsync({ settingFollowRedirects }); updateWorkspace.mutate({ settingFollowRedirects });
}} }}
/> />
</VStack> </VStack>

View File

@@ -53,7 +53,7 @@ export function SettingsDropdown() {
dialog.show({ dialog.show({
id: 'hotkey', id: 'hotkey',
title: 'Keyboard Shortcuts', title: 'Keyboard Shortcuts',
size: 'sm', size: 'dynamic',
render: () => <KeyboardShortcutsDialog />, render: () => <KeyboardShortcutsDialog />,
}); });
}, },

View File

@@ -512,7 +512,7 @@ function SidebarItems({
className={classNames( className={classNames(
tree.depth > 0 && 'border-l border-background-highlight-secondary', tree.depth > 0 && 'border-l border-background-highlight-secondary',
tree.depth === 0 && 'ml-0', tree.depth === 0 && 'ml-0',
tree.depth >= 1 && 'ml-[1.2em]', tree.depth >= 1 && 'ml-[1.2rem]',
)} )}
> >
{tree.children.map((child, i) => { {tree.children.map((child, i) => {
@@ -811,7 +811,7 @@ const SidebarItem = forwardRef(function SidebarItem(
data-active={isActive} data-active={isActive}
data-selected={selected} data-selected={selected}
className={classNames( className={classNames(
'w-full flex gap-1.5 items-center text-sm h-xs px-1.5 rounded-md', 'w-full flex gap-1.5 items-center h-xs px-1.5 rounded-md',
editing && 'ring-1 focus-within:ring-focus', editing && 'ring-1 focus-within:ring-focus',
isActive && 'bg-background-highlight-secondary text-fg', isActive && 'bg-background-highlight-secondary text-fg',
!isActive && !isActive &&
@@ -855,7 +855,7 @@ const SidebarItem = forwardRef(function SidebarItem(
{isResponseLoading(latestHttpResponse) ? ( {isResponseLoading(latestHttpResponse) ? (
<Icon spin size="sm" icon="refresh" className="text-fg-subtler" /> <Icon spin size="sm" icon="refresh" className="text-fg-subtler" />
) : ( ) : (
<StatusTag className="text-2xs" response={latestHttpResponse} /> <StatusTag className="text-xs" response={latestHttpResponse} />
)} )}
</div> </div>
) : null} ) : null}

View File

@@ -130,7 +130,7 @@ export default function Workspace() {
'grid w-full h-full', 'grid w-full h-full',
// Animate sidebar width changes but only when not resizing // Animate sidebar width changes but only when not resizing
// because it's too slow to animate on mouse move // because it's too slow to animate on mouse move
!isResizing && 'transition-all', !isResizing && 'transition-grid',
)} )}
> >
{floating ? ( {floating ? (

View File

@@ -71,7 +71,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
justify === 'start' && 'justify-start', justify === 'start' && 'justify-start',
justify === 'center' && 'justify-center', justify === 'center' && 'justify-center',
size === 'md' && 'h-md px-3 rounded-md', size === 'md' && 'h-md px-3 rounded-md',
size === 'sm' && 'h-sm px-2.5 text-sm rounded-md', size === 'sm' && 'h-sm px-2.5 rounded-md',
size === 'xs' && 'h-xs px-2 text-sm rounded-md', size === 'xs' && 'h-xs px-2 text-sm rounded-md',
size === '2xs' && 'h-5 px-1 text-xs rounded', size === '2xs' && 'h-5 px-1 text-xs rounded',

View File

@@ -28,7 +28,7 @@ export function Checkbox({
as="label" as="label"
space={2} space={2}
alignItems="center" alignItems="center"
className={classNames(className, 'text-fg text-sm', disabled && 'opacity-disabled')} className={classNames(className, 'text-fg', disabled && 'opacity-disabled')}
> >
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}> <div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}>
<input <input

View File

@@ -448,16 +448,14 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
<HStack <HStack
space={2} space={2}
alignItems="center" alignItems="center"
className="pb-0.5 px-1.5 mb-2 text-xs border border-background-highlight-secondary mx-2 rounded font-mono h-2xs" className="pb-0.5 px-1.5 mb-2 text-sm border border-background-highlight-secondary mx-2 rounded font-mono h-2xs"
> >
<Icon icon="search" size="xs" className="text-fg-subtle" /> <Icon icon="search" size="xs" className="text-fg-subtle" />
<div className="text-fg">{filter}</div> <div className="text-fg">{filter}</div>
</HStack> </HStack>
)} )}
{filteredItems.length === 0 && ( {filteredItems.length === 0 && (
<span className="text-fg-subtler text-sm text-center px-2 py-1"> <span className="text-fg-subtler text-center px-2 py-1">No matches</span>
No matches
</span>
)} )}
{filteredItems.map((item, i) => { {filteredItems.map((item, i) => {
if (item.type === 'separator') { if (item.type === 'separator') {
@@ -531,14 +529,18 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
onFocus={handleFocus} onFocus={handleFocus}
onClick={handleClick} onClick={handleClick}
justify="start" justify="start"
leftSlot={item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>} leftSlot={
item.leftSlot && (
<div className="pr-2 flex justify-start text-fg-subtle">{item.leftSlot}</div>
)
}
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>} rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
innerClassName="!text-left" innerClassName="!text-left"
color="custom" color="custom"
className={classNames( className={classNames(
className, className,
'h-xs', // More compact 'h-xs', // More compact
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm whitespace-nowrap', 'min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap',
'focus:bg-background-highlight focus:text-fg rounded', 'focus:bg-background-highlight focus:text-fg rounded',
item.variant === 'default' && 'text-fg-subtle', item.variant === 'default' && 'text-fg-subtle',
item.variant === 'danger' && 'text-fg-danger', item.variant === 'danger' && 'text-fg-danger',

View File

@@ -104,8 +104,7 @@
} }
.cm-scroller { .cm-scroller {
@apply font-mono text-[0.75rem]; @apply font-mono text-editor;
/* /*
* Round corners or they'll stick out of the editor bounds of editor is rounded. * Round corners or they'll stick out of the editor bounds of editor is rounded.
* Could potentially be pushed up from the editor like we do with bg color but this * Could potentially be pushed up from the editor like we do with bg color but this
@@ -185,7 +184,7 @@
} }
.cm-tooltip.cm-tooltip-hover { .cm-tooltip.cm-tooltip-hover {
@apply shadow-lg bg-background rounded text-fg-subtle border border-fg-subtler z-50 pointer-events-auto text-xs; @apply shadow-lg bg-background rounded text-fg-subtle border border-fg-subtler z-50 pointer-events-auto text-sm;
@apply px-2 py-1; @apply px-2 py-1;
a { a {
@@ -208,7 +207,7 @@
/* NOTE: Extra selector required to override default styles */ /* NOTE: Extra selector required to override default styles */
.cm-tooltip.cm-tooltip-autocomplete, .cm-tooltip.cm-tooltip-autocomplete,
.cm-tooltip.cm-completionInfo { .cm-tooltip.cm-completionInfo {
@apply shadow-lg bg-background rounded text-fg-subtle border border-background-highlight z-50 pointer-events-auto text-xs; @apply shadow-lg bg-background rounded text-fg-subtle border border-background-highlight z-50 pointer-events-auto text-sm;
.cm-completionIcon { .cm-completionIcon {
@apply italic font-mono; @apply italic font-mono;
@@ -267,7 +266,7 @@
} }
&.cm-completionInfo-right { &.cm-completionInfo-right {
@apply ml-1 -mt-0.5 text-sm; @apply ml-1 -mt-0.5;
} }
&.cm-completionInfo-right-narrow { &.cm-completionInfo-right-narrow {
@@ -279,12 +278,14 @@
} }
&.cm-tooltip-autocomplete { &.cm-tooltip-autocomplete {
@apply font-mono text-editor;
& > ul { & > ul {
@apply p-1 max-h-[40vh]; @apply p-1 max-h-[40vh];
} }
& > ul > li { & > ul > li {
@apply cursor-default px-2 rounded-sm text-fg-subtle h-7 flex items-center; @apply cursor-default px-2 py-1.5 rounded-sm text-fg-subtle flex items-center;
} }
& > ul > li[aria-selected] { & > ul > li[aria-selected] {
@@ -292,7 +293,7 @@
} }
.cm-completionIcon { .cm-completionIcon {
@apply text-xs flex items-center pb-0.5 flex-shrink-0; @apply text-sm flex items-center pb-0.5 flex-shrink-0;
} }
.cm-completionLabel { .cm-completionLabel {

View File

@@ -9,7 +9,7 @@ export function FormattedError({ children }: Props) {
return ( return (
<pre <pre
className={classNames( className={classNames(
'w-full text-sm select-auto cursor-text bg-gray-100 p-3 rounded', 'w-full select-auto cursor-text bg-gray-100 p-3 rounded',
'whitespace-pre-wrap border border-fg-danger border-dashed overflow-x-auto', 'whitespace-pre-wrap border border-fg-danger border-dashed overflow-x-auto',
)} )}
> >

View File

@@ -7,5 +7,5 @@ interface Props {
export function HotKeyLabel({ action }: Props) { export function HotKeyLabel({ action }: Props) {
const label = useHotKeyLabel(action); const label = useHotKeyLabel(action);
return <span className="text-fg-subtle">{label}</span>; return <span className="text-fg-subtle whitespace-nowrap">{label}</span>;
} }

View File

@@ -11,7 +11,7 @@ interface Props {
export const HotKeyList = ({ hotkeys, bottomSlot }: Props) => { export const HotKeyList = ({ hotkeys, bottomSlot }: Props) => {
return ( return (
<div className="h-full flex items-center justify-center text-sm"> <div className="h-full flex items-center justify-center">
<VStack space={2}> <VStack space={2}>
{hotkeys.map((hotkey) => ( {hotkeys.map((hotkey) => (
<HStack key={hotkey} className="grid grid-cols-2"> <HStack key={hotkey} className="grid grid-cols-2">

View File

@@ -27,7 +27,7 @@ export function HttpMethodTag({ request, className }: Props) {
const m = method.toLowerCase(); const m = method.toLowerCase();
return ( return (
<span className={classNames(className, 'text-2xs font-mono text-fg-subtle')}> <span className={classNames(className, 'text-xs font-mono text-fg-subtle')}>
{methodMap[m] ?? m.slice(0, 3).toUpperCase()} {methodMap[m] ?? m.slice(0, 3).toUpperCase()}
</span> </span>
); );

View File

@@ -6,8 +6,8 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
<code <code
className={classNames( className={classNames(
className, className,
'font-mono text-xs bg-background-highlight-secondary border border-background-highlight', 'font-mono text-shrink bg-background-highlight-secondary border border-background-highlight',
'px-1.5 py-0.5 rounded text-fg-info shadow-inner', 'px-1.5 py-0.5 rounded text-fg shadow-inner',
)} )}
{...props} {...props}
/> />

View File

@@ -133,11 +133,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
> >
<label <label
htmlFor={id} htmlFor={id}
className={classNames( className={classNames(labelClassName, 'text-fg whitespace-nowrap', hideLabel && 'sr-only')}
labelClassName,
'text-sm text-fg whitespace-nowrap',
hideLabel && 'sr-only',
)}
> >
{label} {label}
</label> </label>

View File

@@ -80,7 +80,7 @@ export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPa
<span className={classNames(labelClassName, 'select-text group-hover:text-fg')}>{label}</span> <span className={classNames(labelClassName, 'select-text group-hover:text-fg')}>{label}</span>
); );
return ( return (
<div className={classNames(/*depth === 0 && '-ml-4',*/ 'font-mono text-2xs')}> <div className={classNames(/*depth === 0 && '-ml-4',*/ 'font-mono text-xs')}>
<div className="flex items-center"> <div className="flex items-center">
{isExpandable ? ( {isExpandable ? (
<button className="group relative flex items-center pl-4 w-full" onClick={toggleExpanded}> <button className="group relative flex items-center pl-4 w-full" onClick={toggleExpanded}>

View File

@@ -389,7 +389,7 @@ function PairEditorRow({
<Button <Button
size="xs" size="xs"
color="secondary" color="secondary"
className="font-mono text-xs" className="font-mono text-sm"
onClick={async (e) => { onClick={async (e) => {
e.preventDefault(); e.preventDefault();
const selected = await open({ const selected = await open({

View File

@@ -0,0 +1,148 @@
import classNames from 'classnames';
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import { IconButton } from './IconButton';
import type { InputProps } from './Input';
import { HStack } from './Stacks';
export type PlainInputProps = Omit<InputProps, 'wrapLines' | 'onKeyDown' | 'type'> & {
type: 'text' | 'password' | 'number';
};
export const PlainInput = forwardRef<HTMLInputElement, PlainInputProps>(function Input(
{
className,
containerClassName,
defaultValue,
forceUpdateKey,
hideLabel,
label,
labelClassName,
labelPosition = 'top',
leftSlot,
name,
onBlur,
onChange,
onFocus,
onPaste,
placeholder,
require,
rightSlot,
size = 'md',
type = 'text',
validate,
...props
}: PlainInputProps,
ref,
) {
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
const [focused, setFocused] = useState(false);
const handleFocus = useCallback(() => {
setFocused(true);
onFocus?.();
}, [onFocus]);
const handleBlur = useCallback(() => {
setFocused(false);
onBlur?.();
}, [onBlur]);
const id = `input-${name}`;
const inputClassName = classNames(
className,
'!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder',
'px-1.5 text-xs font-mono',
);
const isValid = useMemo(() => {
if (require && !validateRequire(currentValue)) return false;
if (validate && !validate(currentValue)) return false;
return true;
}, [currentValue, validate, require]);
const handleChange = useCallback(
(value: string) => {
setCurrentValue(value);
onChange?.(value);
},
[onChange],
);
const wrapperRef = useRef<HTMLDivElement>(null);
return (
<div
ref={wrapperRef}
className={classNames(
'w-full',
'pointer-events-auto', // Just in case we're placing in disabled parent
labelPosition === 'left' && 'flex items-center gap-2',
labelPosition === 'top' && 'flex-row gap-0.5',
)}
>
<label
htmlFor={id}
className={classNames(labelClassName, 'text-fg whitespace-nowrap', hideLabel && 'sr-only')}
>
{label}
</label>
<HStack
alignItems="stretch"
className={classNames(
containerClassName,
'x-theme-input',
'relative w-full rounded-md text-fg',
'border',
focused ? 'border-border-focus' : 'border-background-highlight',
!isValid && '!border-fg-danger',
size === 'md' && 'min-h-md',
size === 'sm' && 'min-h-sm',
size === 'xs' && 'min-h-xs',
)}
>
{leftSlot}
<HStack
alignItems="center"
className={classNames(
'w-full min-w-0',
leftSlot && 'pl-0.5 -ml-2',
rightSlot && 'pr-0.5 -mr-2',
)}
>
<input
ref={ref}
key={forceUpdateKey}
id={id}
type={type === 'password' && !obscured ? 'text' : type}
defaultValue={defaultValue}
placeholder={placeholder}
onChange={(e) => handleChange(e.target.value)}
onPaste={(e) => onPaste?.(e.clipboardData.getData('Text'))}
className={inputClassName}
onFocus={handleFocus}
onBlur={handleBlur}
{...props}
/>
</HStack>
{type === 'password' && (
<IconButton
title={obscured ? `Show ${label}` : `Obscure ${label}`}
size="xs"
className="mr-0.5 group/obscure !h-auto my-0.5"
iconClassName="text-fg-subtle group-hover/obscure:text-fg"
iconSize="sm"
icon={obscured ? 'eye' : 'eyeClosed'}
onClick={() => setObscured((o) => !o)}
/>
)}
{rightSlot}
</HStack>
</div>
);
});
function validateRequire(v: string) {
return v.length > 0;
}

View File

@@ -45,11 +45,7 @@ export function Select<T extends string>({
> >
<label <label
htmlFor={id} htmlFor={id}
className={classNames( className={classNames(labelClassName, 'text-fg whitespace-nowrap', hideLabel && 'sr-only')}
labelClassName,
'text-sm text-fg whitespace-nowrap',
hideLabel && 'sr-only',
)}
> >
{label} {label}
</label> </label>
@@ -58,7 +54,7 @@ export function Select<T extends string>({
style={selectBackgroundStyles} style={selectBackgroundStyles}
onChange={(e) => onChange(e.target.value as T)} onChange={(e) => onChange(e.target.value as T)}
className={classNames( className={classNames(
'font-mono text-xs border w-full outline-none bg-transparent pl-2 pr-7', 'font-mono text-sm border w-full outline-none bg-transparent pl-2 pr-7',
'bg-background-highlight-secondary border-background-highlight focus:border-border-focus', 'bg-background-highlight-secondary border-background-highlight focus:border-border-focus',
size === 'xs' && 'h-xs', size === 'xs' && 'h-xs',
size === 'sm' && 'h-sm', size === 'sm' && 'h-sm',

View File

@@ -11,7 +11,7 @@ interface Props {
export function Separator({ className, orientation = 'horizontal', children }: Props) { export function Separator({ className, orientation = 'horizontal', children }: Props) {
return ( return (
<div role="separator" className={classNames(className, 'flex items-center')}> <div role="separator" className={classNames(className, 'flex items-center')}>
{children && <div className="text-xs text-fg-subtler mr-2 whitespace-nowrap">{children}</div>} {children && <div className="text-sm text-fg-subtler mr-2 whitespace-nowrap">{children}</div>}
<div <div
className={classNames( className={classNames(
'bg-background-highlight', 'bg-background-highlight',

View File

@@ -84,7 +84,7 @@ export function Tabs({
{tabs.map((t) => { {tabs.map((t) => {
const isActive = t.value === value; const isActive = t.value === value;
const btnClassName = classNames( const btnClassName = classNames(
'h-full flex items-center text-sm rounded', 'h-full flex items-center rounded',
'!px-2 ml-[1px]', '!px-2 ml-[1px]',
addBorders && 'border', addBorders && 'border',
isActive ? 'text-fg' : 'text-fg-subtle hover:text-fg', isActive ? 'text-fg' : 'text-fg-subtle hover:text-fg',

View File

@@ -20,7 +20,7 @@ export function ImageViewer({ response, className }: Props) {
if (!show) { if (!show) {
return ( return (
<> <>
<div className="text-sm italic text-fg-subtler"> <div className="italic text-fg-subtler">
Response body is too large to preview.{' '} Response body is too large to preview.{' '}
<button className="cursor-pointer underline hover:text-fg" onClick={() => setShow(true)}> <button className="cursor-pointer underline hover:text-fg" onClick={() => setShow(true)}>
Show anyway Show anyway

View File

@@ -20,7 +20,10 @@ export type HotkeyAction =
| 'sidebar.focus' | 'sidebar.focus'
| 'sidebar.toggle' | 'sidebar.toggle'
| 'urlBar.focus' | 'urlBar.focus'
| 'command_palette.toggle'; | 'command_palette.toggle'
| 'app.zoom_in'
| 'app.zoom_out'
| 'app.zoom_reset';
const hotkeys: Record<HotkeyAction, string[]> = { const hotkeys: Record<HotkeyAction, string[]> = {
'environmentEditor.toggle': ['CmdCtrl+Shift+e'], 'environmentEditor.toggle': ['CmdCtrl+Shift+e'],
@@ -37,6 +40,9 @@ const hotkeys: Record<HotkeyAction, string[]> = {
'sidebar.toggle': ['CmdCtrl+b'], 'sidebar.toggle': ['CmdCtrl+b'],
'urlBar.focus': ['CmdCtrl+l'], 'urlBar.focus': ['CmdCtrl+l'],
'command_palette.toggle': ['CmdCtrl+k'], 'command_palette.toggle': ['CmdCtrl+k'],
'app.zoom_in': ['CmdCtrl+='],
'app.zoom_out': ['CmdCtrl+-'],
'app.zoom_reset': ['CmdCtrl+0'],
}; };
const hotkeyLabels: Record<HotkeyAction, string> = { const hotkeyLabels: Record<HotkeyAction, string> = {
@@ -54,6 +60,9 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
'sidebar.toggle': 'Toggle Sidebar', 'sidebar.toggle': 'Toggle Sidebar',
'urlBar.focus': 'Focus URL', 'urlBar.focus': 'Focus URL',
'command_palette.toggle': 'Toggle Command Palette', 'command_palette.toggle': 'Toggle Command Palette',
'app.zoom_in': 'Zoom In',
'app.zoom_out': 'Zoom Out',
'app.zoom_reset': 'Zoom to Actual Size',
}; };
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[]; export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];

View File

@@ -1,17 +1,17 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
type Appearance, getCSSAppearance,
getPreferredAppearance, getWindowAppearance,
subscribeToPreferredAppearanceChange, subscribeToWindowAppearanceChange,
} from '../lib/theme/window'; } from '../lib/theme/appearance';
import { type Appearance } from '../lib/theme/window';
export function usePreferredAppearance() { export function usePreferredAppearance() {
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(); const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(getCSSAppearance());
// Set appearance when preferred theme changes
useEffect(() => { useEffect(() => {
getPreferredAppearance().then(setPreferredAppearance); getWindowAppearance().then(setPreferredAppearance);
return subscribeToPreferredAppearanceChange(setPreferredAppearance); return subscribeToWindowAppearanceChange(setPreferredAppearance);
}, []); }, []);
return preferredAppearance; return preferredAppearance;

View File

@@ -10,7 +10,5 @@ export function useResolvedAppearance() {
? preferredAppearance ? preferredAppearance
: settings.appearance; : settings.appearance;
console.log('HELLO', settings?.appearance, preferredAppearance);
return appearance; return appearance;
} }

31
src-web/hooks/useZoom.ts Normal file
View File

@@ -0,0 +1,31 @@
import { useCallback } from 'react';
import { useSettings } from './useSettings';
import { useUpdateSettings } from './useUpdateSettings';
export function useZoom() {
const settings = useSettings();
const updateSettings = useUpdateSettings();
const zoomIn = useCallback(() => {
if (!settings) return;
updateSettings.mutate({
...settings,
interfaceScale: Math.min(1.8, settings.interfaceScale * 1.1),
});
}, [settings, updateSettings]);
const zoomOut = useCallback(() => {
if (!settings) return;
updateSettings.mutate({
...settings,
interfaceScale: Math.max(0.4, settings.interfaceScale * 0.9),
});
}, [settings, updateSettings]);
const zoomReset = useCallback(() => {
if (!settings) return;
updateSettings.mutate({ ...settings, interfaceScale: 1 });
}, [settings, updateSettings]);
return { zoomIn, zoomOut, zoomReset };
}

View File

@@ -1,6 +0,0 @@
export function indent(text: string, space = ' '): string {
return text
.split('\n')
.map((line) => space + line)
.join('\n');
}

View File

@@ -37,6 +37,10 @@ export interface Settings extends BaseModel {
themeLight: string; themeLight: string;
themeDark: string; themeDark: string;
updateChannel: string; updateChannel: string;
interfaceFontSize: number;
interfaceScale: number;
editorFontSize: number;
editorSoftWrap: number;
} }
export interface Workspace extends BaseModel { export interface Workspace extends BaseModel {

View File

@@ -0,0 +1,31 @@
import { getCurrent } from '@tauri-apps/api/webviewWindow';
import type { Appearance } from './window';
export function getCSSAppearance(): Appearance {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
export async function getWindowAppearance(): Promise<Appearance> {
const a = await getCurrent().theme();
return a ?? getCSSAppearance();
}
/**
* Subscribe to appearance (dark/light) changes. Note, we use Tauri Window appearance instead of
* CSS appearance because CSS won't fire the way we handle window theme management.
*/
export function subscribeToWindowAppearanceChange(
cb: (appearance: Appearance) => void,
): () => void {
const container = { unsubscribe: () => {} };
getCurrent()
.onThemeChanged((t) => {
cb(t.payload);
})
.then((l) => {
container.unsubscribe = l;
});
return () => container.unsubscribe();
}

View File

@@ -41,22 +41,10 @@ export class Color {
return this.theme === 'dark' ? this._darken(mod) : this._lighten(mod); return this.theme === 'dark' ? this._darken(mod) : this._lighten(mod);
} }
lowerTo(value: number): Color {
return this.theme === 'dark'
? this._darken(1)._lighten(value)
: this._lighten(1)._darken(1 - value);
}
lift(mod: number): Color { lift(mod: number): Color {
return this.theme === 'dark' ? this._lighten(mod) : this._darken(mod); return this.theme === 'dark' ? this._lighten(mod) : this._darken(mod);
} }
liftTo(value: number): Color {
return this.theme === 'dark'
? this._lighten(1)._darken(1 - value)
: this._darken(1)._lighten(value);
}
translucify(mod: number): Color { translucify(mod: number): Color {
const c = this.clone(); const c = this.clone();
c.alpha = c.alpha - c.alpha * mod; c.alpha = c.alpha - c.alpha * mod;

View File

@@ -31,10 +31,10 @@ export const yaakLight: YaakTheme = {
export const yaakDark: YaakTheme = { export const yaakDark: YaakTheme = {
id: 'yaak-dark', id: 'yaak-dark',
name: 'Yaak', name: 'Yaak',
background: new Color('hsl(244,23%,13%)', 'dark'), background: new Color('hsl(244,23%,14%)', 'dark'),
backgroundHighlight: new Color('hsl(244,23%,23%)', 'dark'), backgroundHighlight: new Color('hsl(244,23%,23%)', 'dark'),
backgroundHighlightSecondary: new Color('hsl(244,23%,20%)', 'dark'), backgroundHighlightSecondary: new Color('hsl(244,23%,20%)', 'dark'),
foreground: new Color('hsl(245,23%,86%)', 'dark'), foreground: new Color('hsl(245,23%,80%)', 'dark'),
foregroundSubtle: new Color('hsl(245,20%,65%)', 'dark'), foregroundSubtle: new Color('hsl(245,20%,65%)', 'dark'),
foregroundSubtler: new Color('hsl(245,18%,50%)', 'dark'), foregroundSubtler: new Color('hsl(245,18%,50%)', 'dark'),

View File

@@ -1,5 +1,3 @@
import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { indent } from '../indent';
import { Color } from './color'; import { Color } from './color';
export type Appearance = 'dark' | 'light' | 'system'; export type Appearance = 'dark' | 'light' | 'system';
@@ -238,24 +236,9 @@ export function setThemeOnDocument(theme: YaakTheme) {
document.documentElement.setAttribute('data-theme', theme.id); document.documentElement.setAttribute('data-theme', theme.id);
} }
export async function getPreferredAppearance(): Promise<Appearance> { export function indent(text: string, space = ' '): string {
const a = await getCurrent().theme(); return text
return a ?? 'light'; .split('\n')
} .map((line) => space + line)
.join('\n');
export function subscribeToPreferredAppearanceChange(
cb: (appearance: Appearance) => void,
): () => void {
const container = { unsubscribe: () => {} };
getCurrent()
.onThemeChanged((t) => {
console.log('THEME CHANGED', t);
cb(t.payload);
})
.then((l) => {
container.unsubscribe = l;
});
return () => container.unsubscribe();
} }

View File

@@ -38,7 +38,7 @@
.hide-scrollbars { .hide-scrollbars {
&::-webkit-scrollbar-corner, &::-webkit-scrollbar-corner,
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none !important; display: NONE !important;
} }
} }

View File

@@ -1,10 +1,10 @@
const plugin = require('tailwindcss/plugin'); const plugin = require('tailwindcss/plugin');
const height = { const height = {
'2xs': '1.5rem', '2xs': '1.6rem',
xs: '1.75rem', xs: '1.8rem',
sm: '2.0rem', sm: '2.2rem',
md: '2.5rem', md: '2.7rem',
}; };
/** @type {import("tailwindcss").Config} */ /** @type {import("tailwindcss").Config} */
@@ -27,11 +27,14 @@ module.exports = {
sm: 'calc(2.0rem - 2px)', sm: 'calc(2.0rem - 2px)',
md: 'calc(2.5rem - 2px)', md: 'calc(2.5rem - 2px)',
}, },
transitionProperty: {
grid: 'grid',
},
}, },
fontFamily: { fontFamily: {
mono: ['JetBrains Mono', 'Menlo', 'monospace'], mono: ['JetBrains Mono', 'Menlo', 'monospace'],
sans: [ sans: [
'Inter', 'Inter UI',
'-apple-system', '-apple-system',
'BlinkMacSystemFont', 'BlinkMacSystemFont',
'Segoe UI', 'Segoe UI',
@@ -58,6 +61,8 @@ module.exports = {
'3xl': '2rem', '3xl': '2rem',
'4xl': '2.5rem', '4xl': '2.5rem',
'5xl': '3rem', '5xl': '3rem',
'editor': 'var(--editor-font-size)',
'shrink': '0.8em',
}, },
boxShadow: { boxShadow: {
DEFAULT: '0 1px 3px 0 var(--shadow);', DEFAULT: '0 1px 3px 0 var(--shadow);',