mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 01:28:35 +02:00
Custom font sizes and better zoom
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
12
src-tauri/.sqlx/query-efd8ba41ea909b18dd520c57c1d464c5ae057b720cbbedcaec1513d43535632c.json
generated
Normal file
12
src-tauri/.sqlx/query-efd8ba41ea909b18dd520c57c1d464c5ae057b720cbbedcaec1513d43535632c.json
generated
Normal 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"
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"]}}
|
||||||
4
src-tauri/migrations/20240529143147_more-settings.sql
Normal file
4
src-tauri/migrations/20240529143147_more-settings.sql
Normal 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;
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -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 */}
|
||||||
‎
|
‎
|
||||||
{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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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));
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function RecentConnectionsDropdown({
|
|||||||
label: (
|
label: (
|
||||||
<HStack space={2} alignItems="center">
|
<HStack space={2} alignItems="center">
|
||||||
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago •{' '}
|
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago •{' '}
|
||||||
<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" />,
|
||||||
|
|||||||
@@ -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',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>→</span>{' '}
|
<span>→</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" />,
|
||||||
|
|||||||
@@ -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',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 />,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
148
src-web/components/core/PlainInput.tsx
Normal file
148
src-web/components/core/PlainInput.tsx
Normal 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;
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)[];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
31
src-web/hooks/useZoom.ts
Normal 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 };
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export function indent(text: string, space = ' '): string {
|
|
||||||
return text
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => space + line)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
31
src-web/lib/theme/appearance.ts
Normal file
31
src-web/lib/theme/appearance.ts
Normal 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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
.hide-scrollbars {
|
.hide-scrollbars {
|
||||||
&::-webkit-scrollbar-corner,
|
&::-webkit-scrollbar-corner,
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none !important;
|
display: NONE !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);',
|
||||||
|
|||||||
Reference in New Issue
Block a user