Compare commits

...

27 Commits

Author SHA1 Message Date
Gregory Schier
6b60c86300 macOS 12 2023-04-06 08:39:30 -07:00
Gregory Schier
30c1b5e8c7 Remove system tray icon 2023-04-06 08:15:40 -07:00
Gregory Schier
10af9b6f99 Minor tweaks 2023-04-04 17:21:02 -07:00
Gregory Schier
aa8c066f2d Fix some things 2023-04-04 16:56:45 -07:00
Gregory Schier
b913b74449 Editor line wrapping support (not used yet) 2023-04-04 16:40:37 -07:00
Gregory Schier
b71adce50b remove janky last location tracking 2023-04-04 16:23:08 -07:00
Gregory Schier
0fbb44c701 Fix resize cursor 2023-04-04 16:12:45 -07:00
Gregory Schier
de335e8637 Better button styles 2023-04-04 15:40:25 -07:00
Gregory Schier
2999f63a4c Bump version 2023-04-04 13:56:24 -07:00
Gregory Schier
2abc5e6f0b Some small fixes 2023-04-04 13:56:14 -07:00
Gregory Schier
639de4321e A few fixes 2023-04-04 13:31:48 -07:00
Gregory Schier
b3c461afdd Better status tags and delete request on key 2023-04-04 12:36:30 -07:00
Gregory Schier
7d154800a0 Remove expects from request sending 2023-04-04 08:14:32 -07:00
Gregory Schier
b48ed0399e Fix web view height 2023-04-04 07:51:41 -07:00
Gregory Schier
c5d6e7d74a Fix autocomplete spacing 2023-04-04 07:51:19 -07:00
Gregory Schier
e82f915363 Fix input focus border 2023-04-03 12:19:37 -07:00
Gregory Schier
3128e9ce76 Hot keys and cleanup 2023-04-03 07:59:49 -07:00
Gregory Schier
bc0e86757c Add entitlemet for v8 2023-04-02 20:23:21 -07:00
Gregory Schier
fec99916c2 Debug codesigned build 2023-04-02 19:09:14 -07:00
Gregory Schier
3b5d059b11 Disable code signing 2023-04-02 18:27:14 -07:00
Gregory Schier
c3fe2acc8a Fix tauri script command 2023-04-02 17:25:24 -07:00
Gregory Schier
4d002c412b Fix universal binary 2023-04-02 17:12:20 -07:00
Gregory Schier
46d152b5f1 Bump version 2023-04-02 15:44:41 -07:00
Gregory Schier
25fa81ebbc Fix toolchain 2023-04-02 15:44:21 -07:00
Gregory Schier
7c2de3c360 Add proper target 2023-04-02 15:42:19 -07:00
Gregory Schier
3a3b187cd0 Try universal binary 2023-04-02 15:33:13 -07:00
Gregory Schier
3226bbe083 Fix version 2023-04-02 15:25:24 -07:00
43 changed files with 404 additions and 387 deletions

View File

@@ -5,17 +5,22 @@ on:
jobs:
build-artifacts:
runs-on: ${{ matrix.platform }}
strategy:
fail-fast: false
matrix:
# platform: [ ubuntu-latest, macos-latest, windows-latest ]
platform: [ macos-latest ]
include:
- os: macos-12
target: aarch64-apple-darwin
- os: macos-12
target: x86_64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust
uses: actions/cache@v2
with:
@@ -29,7 +34,7 @@ jobs:
node-version: 18
cache: 'npm'
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
@@ -37,7 +42,9 @@ jobs:
run: npm ci
- name: Run tests
run: npm test
- uses: tauri-apps/tauri-action@v0
# Pin dev version to get non-default targets
# https://github.com/tauri-apps/tauri-action/issues/356
- uses: tauri-apps/tauri-action@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
@@ -49,8 +56,9 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
with:
tagName: v__VERSION__
releaseName: 'App v__VERSION__'
releaseBody: 'See the assets to download this version and install.'
tagName: 'v__VERSION__'
releaseName: 'Release __VERSION__'
releaseBody: '<!-- Release Notes -->'
releaseDraft: true
prerelease: false
args: '--target ${{ matrix.target }}'

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Yaak App</title>
<script src="http://localhost:8097"></script>
<!-- <script src="http://localhost:8097"></script>-->
<style>
body {
background-color: white;

View File

@@ -6,6 +6,7 @@
"scripts": {
"tauri-dev": "YAAK_ENV=development tauri dev",
"tauri-build": "tauri build",
"tauri": "tauri",
"build": "npm run build:frontend",
"dev": "vite dev",
"lint": "tsc && eslint . --ext .ts,.tsx",

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

View File

@@ -22,7 +22,7 @@ use sqlx::types::Json;
use sqlx::{Pool, Sqlite};
use tauri::regex::Regex;
use tauri::{AppHandle, Menu, MenuItem, RunEvent, State, Submenu, TitleBarStyle, Window, Wry};
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
use tauri::{CustomMenuItem, Manager, WindowEvent};
use tokio::sync::Mutex;
use window_ext::WindowExt;
@@ -79,11 +79,8 @@ async fn actually_send_ephemeral_request(
let start = std::time::Instant::now();
let mut url_string = request.url.to_string();
let mut variables = HashMap::new();
variables.insert("PROJECT_ID", "project_123");
variables.insert("TOKEN", "s3cret");
variables.insert("DOMAIN", "schier.co");
variables.insert("BASE_URL", "https://schier.co");
let variables: HashMap<&str, &str> = HashMap::new();
// variables.insert("", "");
let re = Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}").expect("Failed to create regex");
url_string = re
@@ -104,6 +101,7 @@ async fn actually_send_ephemeral_request(
let client = reqwest::Client::builder()
.redirect(Policy::none())
// .danger_accept_invalid_certs(true)
.build()
.expect("Failed to build client");
@@ -188,12 +186,23 @@ async fn actually_send_ephemeral_request(
let raw_response = client.execute(sendable_req).await;
let p = app_handle
.path_resolver()
.resolve_resource("plugins/plugin.ts")
.expect("failed to resolve resource");
let plugin_rel_path = "plugins/plugin.ts";
let plugin_path = match app_handle.path_resolver().resolve_resource(plugin_rel_path) {
Some(p) => p,
None => {
return response_err(
response,
format!("Plugin not found at {}", plugin_rel_path),
&app_handle,
pool,
)
.await;
}
};
runtime::run_plugin_sync(p.to_str().unwrap()).unwrap();
if let Err(e) = runtime::run_plugin_sync(plugin_path.to_str().unwrap()) {
return response_err(response, e.to_string(), &app_handle, pool).await;
}
match raw_response {
Ok(v) => {
@@ -500,12 +509,7 @@ fn greet(name: &str) -> String {
}
fn main() {
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
let tray_menu = SystemTrayMenu::new().add_item(quit);
let system_tray = SystemTray::new().with_menu(tray_menu);
tauri::Builder::default()
.system_tray(system_tray)
.setup(|app| {
let dir = match is_dev() {
true => current_dir().unwrap(),
@@ -533,20 +537,6 @@ fn main() {
Ok(())
})
})
.on_system_tray_event(|app, event| {
if let SystemTrayEvent::MenuItemClick { id, .. } = event {
match id.as_str() {
"quit" => {
std::process::exit(0);
}
"hide" => {
let window = app.get_window("main").unwrap();
window.hide().unwrap();
}
_ => {}
};
}
})
.invoke_handler(tauri::generate_handler![
greet,
workspaces,
@@ -608,6 +598,14 @@ fn create_window(handle: &AppHandle<Wry>) -> Window<Wry> {
.add_item(
CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
)
.add_item(
CustomMenuItem::new("new_request".to_string(), "New Request")
.accelerator("CmdOrCtrl+n"),
)
.add_item(
CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
.accelerator("CmdOrCtrl+d"),
)
.add_item(CustomMenuItem::new("new_window".to_string(), "New Window"));
if is_dev() {
test_menu = test_menu
@@ -652,6 +650,8 @@ fn create_window(handle: &AppHandle<Wry>) -> Window<Wry> {
"toggle_sidebar" => win2.emit("toggle_sidebar", true).unwrap(),
"focus_url" => win2.emit("focus_url", true).unwrap(),
"send_request" => win2.emit("send_request", true).unwrap(),
"new_request" => _ = win2.emit("new_request", true).unwrap(),
"duplicate_request" => _ = win2.emit("duplicate_request", true).unwrap(),
"refresh" => win2.eval("location.reload()").unwrap(),
"new_window" => _ = create_window(&handle2),
"toggle_devtools" => {

View File

@@ -8,7 +8,7 @@
},
"package": {
"productName": "Yaak",
"version": "2022.0.1"
"version": "2023.0.14"
},
"tauri": {
"windows": [],
@@ -52,6 +52,7 @@
},
"macOS": {
"exceptionDomain": "",
"entitlements": "macos/entitlements.plist",
"frameworks": []
},
"windows": {

View File

@@ -1,7 +1,4 @@
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { MotionConfig } from 'framer-motion';
import { Suspense } from 'react';
import { DndProvider } from 'react-dnd';
@@ -16,26 +13,12 @@ const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 1000 * 60 * 60 * 24, // 24 hours
refetchOnWindowFocus: true,
networkMode: 'offlineFirst',
// It's a desktop app, so this isn't necessary
refetchOnWindowFocus: false,
},
},
});
const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
throttleTime: 1000, // 1 second
});
persistQueryClient({
queryClient,
persister: localStoragePersister,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
});
export function App() {
return (
<QueryClientProvider client={queryClient}>
@@ -46,7 +29,7 @@ export function App() {
<Suspense>
<AppRouter />
<TauriListeners />
<ReactQueryDevtools initialIsOpen={false} />
{/*<ReactQueryDevtools initialIsOpen={false} />*/}
</Suspense>
</DialogProvider>
</DndProvider>

View File

@@ -1,13 +1,5 @@
import { useEffect } from 'react';
import {
createBrowserRouter,
Navigate,
Outlet,
RouterProvider,
useLocation,
} from 'react-router-dom';
import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom';
import { routePaths } from '../hooks/useRoutes';
import { setLastLocation } from '../lib/lastLocation';
import RouteError from './RouteError';
import Workspace from './Workspace';
import Workspaces from './Workspaces';
@@ -16,7 +8,6 @@ const router = createBrowserRouter([
{
path: '/',
errorElement: <RouteError />,
element: <RouterRoot />,
children: [
{
path: '/',
@@ -44,11 +35,3 @@ const router = createBrowserRouter([
export function AppRouter() {
return <RouterProvider router={router} />;
}
function RouterRoot() {
const { pathname } = useLocation();
useEffect(() => {
setLastLocation(pathname).catch(console.error);
}, [pathname]);
return <Outlet />;
}

View File

@@ -71,7 +71,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
const dialog = useDialog();
return (
<div className="pb-2 h-full grid grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<div className="pb-2 h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<Editor
contentType="application/graphql"
defaultValue={query ?? ''}

View File

@@ -0,0 +1,9 @@
interface Props {
data: string;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function ImageView({ data }: Props) {
// const dataUri = `data:image/png;base64,${window.btoa(data)}`;
return <div>Image preview not supported until binary response support is added</div>;
}

View File

@@ -1,11 +1,11 @@
import type { HTMLAttributes, ReactElement } from 'react';
import { useConfirm } from '../hooks/useConfirm';
import React from 'react';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
import { useRequest } from '../hooks/useRequest';
import { useTheme } from '../hooks/useTheme';
import { Dropdown } from './core/Dropdown';
import { HotKey } from './core/HotKey';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
interface Props {
requestId: string;
@@ -13,10 +13,9 @@ interface Props {
}
export function RequestActionsDropdown({ requestId, children }: Props) {
const request = useRequest(requestId ?? null);
const deleteRequest = useDeleteRequest(requestId ?? null);
const deleteRequest = useDeleteRequest(requestId);
const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true });
const confirm = useConfirm();
const { appearance, toggleAppearance } = useTheme();
return (
<Dropdown
@@ -25,25 +24,19 @@ export function RequestActionsDropdown({ requestId, children }: Props) {
label: 'Duplicate',
onSelect: duplicateRequest.mutate,
leftSlot: <Icon icon="copy" />,
rightSlot: <HotKey>D</HotKey>,
},
{
label: 'Delete',
onSelect: async () => {
const confirmed = await confirm({
title: 'Delete Request',
variant: 'delete',
description: (
<>
Are you sure you want to delete <InlineCode>{request?.name}</InlineCode>?
</>
),
});
if (confirmed) {
deleteRequest.mutate();
}
},
onSelect: deleteRequest.mutate,
leftSlot: <Icon icon="trash" />,
},
{ type: 'separator', label: 'Yaak Settings' },
{
label: appearance === 'dark' ? 'Light Theme' : 'Dark Theme',
onSelect: toggleAppearance,
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
},
]}
>
{children}

View File

@@ -30,8 +30,8 @@ export function ResizeHandle({
style={style}
className={classnames(
className,
'group z-10 flex cursor-ew-resize',
vertical ? 'w-full h-3 cursor-ns-resize' : 'h-full w-3 cursor-ew-resize',
'group z-10 flex',
vertical ? 'w-full h-3 cursor-row-resize' : 'h-full w-3 cursor-col-resize',
justify === 'center' && 'justify-center',
justify === 'end' && 'justify-end',
justify === 'start' && 'justify-start',
@@ -46,9 +46,9 @@ export function ResizeHandle({
{isResizing && (
<div
className={classnames(
'fixed -left-20 -right-20 -top-20 -bottom-20 cursor-ew-resize',
vertical && 'cursor-ns-resize',
!vertical && 'cursor-ew-resize',
'fixed -left-20 -right-20 -top-20 -bottom-20',
vertical && 'cursor-row-resize',
!vertical && 'cursor-col-resize',
)}
/>
)}

View File

@@ -8,13 +8,13 @@ interface Props {
export function ResponseHeaders({ headers }: Props) {
return (
<dl className="text-xs w-full font-mono">
<dl className="text-xs w-full h-full font-mono overflow-auto">
{headers.map((h, i) => {
return (
<HStack
space={3}
key={i}
className={classnames(i > 0 && 'border-t border-highlightSecondary', 'py-1')}
className={classnames(i > 0 ? 'border-t border-highlightSecondary py-1' : 'pb-1')}
>
<dd className="w-1/3 text-violet-600 select-text cursor-text">{h.name}</dd>
<dt className="w-2/3 select-text cursor-text break-all">{h.value}</dt>

View File

@@ -9,6 +9,7 @@ import { useResponses } from '../hooks/useResponses';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
import { tryFormatJson } from '../lib/formatters';
import type { HttpResponse } from '../lib/models';
import { isResponseLoading } from '../lib/models';
import { pluralize } from '../lib/pluralize';
import { Banner } from './core/Banner';
import { CountBadge } from './core/CountBadge';
@@ -17,10 +18,12 @@ import { Editor } from './core/Editor';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import { StatusColor } from './core/StatusColor';
import { StatusTag } from './core/StatusTag';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { Webview } from './core/Webview';
import { EmptyStateText } from './EmptyStateText';
import { ImageView } from './ImageView';
import { ResponseHeaders } from './ResponseHeaders';
interface Props {
@@ -52,9 +55,20 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
[activeResponse],
);
const tabs = useMemo(
const tabs: TabItem[] = useMemo(
() => [
{ label: 'Body', value: 'body' },
{
value: 'body',
label: 'Preview',
options: {
value: viewMode,
onChange: toggleViewMode,
items: [
{ label: 'Pretty', value: 'pretty' },
{ label: 'Raw', value: 'raw' },
],
},
},
{
label: (
<div className="flex items-center">
@@ -67,9 +81,12 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
value: 'headers',
},
],
[activeResponse?.headers],
[activeResponse?.headers, toggleViewMode, viewMode],
);
// Don't render until we know the view mode
if (viewMode === undefined) return null;
return (
<div
style={style}
@@ -80,105 +97,104 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
'shadow shadow-gray-100 dark:shadow-gray-0 relative',
)}
>
<HStack
alignItems="center"
className={classnames(
'italic text-gray-700 text-sm w-full flex-shrink-0',
// Remove a bit of space because the tabs have lots too
'-mb-1.5',
)}
>
{activeResponse && (
<>
<div className="whitespace-nowrap p-3 py-2">
<StatusColor statusCode={activeResponse.status}>
{activeResponse.status}
{activeResponse.statusReason && ` ${activeResponse.statusReason}`}
</StatusColor>
&nbsp;&bull;&nbsp;
{activeResponse.elapsed}ms &nbsp;&bull;&nbsp;
{Math.round(activeResponse.body.length / 1000)} KB
</div>
{activeResponse?.error && <Banner className="m-2">{activeResponse.error}</Banner>}
{activeResponse && !activeResponse.error && !isResponseLoading(activeResponse) && (
<>
<HStack
alignItems="center"
className={classnames(
'italic text-gray-700 text-sm w-full flex-shrink-0',
// Remove a bit of space because the tabs have lots too
'-mb-1.5',
)}
>
{activeResponse && (
<HStack alignItems="center" className="w-full">
<div className="whitespace-nowrap px-3">
<StatusTag response={activeResponse} />
{activeResponse.elapsed > 0 && <>&nbsp;&bull;&nbsp;{activeResponse.elapsed}ms</>}
{activeResponse.body.length > 0 && (
<>&nbsp;&bull;&nbsp;{(activeResponse.body.length / 1000).toFixed(1)} KB</>
)}
</div>
<Dropdown
items={[
{
label: viewMode === 'pretty' ? 'View Raw' : 'View Prettified',
onSelect: toggleViewMode,
},
{ type: 'separator', label: 'Actions' },
{
label: 'Clear Response',
onSelect: deleteResponse.mutate,
disabled: responses.length === 0,
},
{
label: `Clear ${responses.length} ${pluralize('Response', responses.length)}`,
onSelect: deleteAllResponses.mutate,
hidden: responses.length <= 1,
disabled: responses.length === 0,
},
{ type: 'separator', label: 'History' },
...responses.slice(0, 10).map((r) => ({
label: r.status + ' - ' + r.elapsed + ' ms',
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <></>,
onSelect: () => setPinnedResponseId(r.id),
})),
]}
<Dropdown
items={[
{
label: 'Clear Response',
onSelect: deleteResponse.mutate,
disabled: responses.length === 0,
},
{
label: `Clear ${responses.length} ${pluralize('Response', responses.length)}`,
onSelect: deleteAllResponses.mutate,
hidden: responses.length <= 1,
disabled: responses.length === 0,
},
{ type: 'separator', label: 'History' },
...responses.slice(0, 10).map((r) => ({
label: r.status + ' - ' + r.elapsed + ' ms',
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <></>,
onSelect: () => setPinnedResponseId(r.id),
})),
]}
>
<IconButton
title="Show response history"
icon="triangleDown"
className="ml-auto"
size="sm"
iconSize="md"
/>
</Dropdown>
</HStack>
)}
</HStack>
{
<Tabs
value={activeTab}
onChangeValue={setActiveTab}
label="Response"
tabs={tabs}
className="ml-3 mr-1"
tabListClassName="mt-1.5"
>
<IconButton
title="Show response history"
icon="triangleDown"
className="ml-auto"
size="sm"
iconSize="md"
/>
</Dropdown>
</>
)}
</HStack>
{activeResponse?.error ? (
<Banner className="m-2">{activeResponse.error}</Banner>
) : (
<Tabs
value={activeTab}
onChangeValue={setActiveTab}
label="Response"
className="px-3"
tabs={tabs}
>
<TabContent value="body">
{activeResponse === null ? (
<EmptyStateText>No Response</EmptyStateText>
) : viewMode === 'pretty' && contentType.includes('html') ? (
<Webview
body={activeResponse.body}
contentType={contentType}
url={activeResponse.url}
/>
) : viewMode === 'pretty' && contentType.includes('json') ? (
<Editor
readOnly
forceUpdateKey={`pretty::${activeResponse.updatedAt}`}
className="bg-gray-50 dark:!bg-gray-100"
defaultValue={tryFormatJson(activeResponse?.body)}
contentType={contentType}
/>
) : activeResponse?.body ? (
<Editor
readOnly
forceUpdateKey={activeResponse.updatedAt}
className="bg-gray-50 dark:!bg-gray-100"
defaultValue={activeResponse?.body}
contentType={contentType}
/>
) : null}
</TabContent>
<TabContent value="headers">
<ResponseHeaders headers={activeResponse?.headers ?? []} />
</TabContent>
</Tabs>
<TabContent value="headers">
<ResponseHeaders headers={activeResponse?.headers ?? []} />
</TabContent>
<TabContent value="body">
{!activeResponse.body ? (
<EmptyStateText>No Response</EmptyStateText>
) : viewMode === 'pretty' && contentType.includes('html') ? (
<Webview
body={activeResponse.body}
contentType={contentType}
url={activeResponse.url}
/>
) : viewMode === 'pretty' && contentType.includes('json') ? (
<Editor
readOnly
forceUpdateKey={`pretty::${activeResponse.updatedAt}`}
className="bg-gray-50 dark:!bg-gray-100"
defaultValue={tryFormatJson(activeResponse?.body)}
contentType={contentType}
/>
) : contentType.startsWith('image') ? (
<ImageView data={activeResponse?.body} />
) : activeResponse?.body ? (
<Editor
readOnly
forceUpdateKey={activeResponse.updatedAt}
className="bg-gray-50 dark:!bg-gray-100"
defaultValue={activeResponse?.body}
contentType={contentType}
/>
) : null}
</TabContent>
</Tabs>
}
</>
)}
</div>
);

View File

@@ -15,9 +15,14 @@ export default function RouteError() {
<pre className="text-sm select-auto cursor-text bg-gray-100 p-3 rounded whitespace-normal">
{message}
</pre>
<Button to="/" color="primary">
Go Home
</Button>
<VStack space={2}>
<Button to="/" color="primary">
Go Home
</Button>
<Button color="secondary" onClick={() => window.location.reload()}>
Refresh
</Button>
</VStack>
</VStack>
</div>
);

View File

@@ -4,16 +4,18 @@ import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useSta
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useLatestResponse } from '../hooks/useLatestResponse';
import { useRequests } from '../hooks/useRequests';
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models';
import { isResponseLoading } from '../lib/models';
import { Button } from './core/Button';
import { IconButton } from './core/IconButton';
import { HStack, VStack } from './core/Stacks';
import { Icon } from './core/Icon';
import { VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag';
import { DropMarker } from './DropMarker';
import { RequestActionsDropdown } from './RequestActionsDropdown';
import { ToggleThemeButton } from './ToggleThemeButton';
interface Props {
className?: string;
@@ -45,9 +47,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
>
<SidebarItems activeRequestId={activeRequest?.id} requests={requests} />
</VStack>
<HStack className="mx-1 pb-1" alignItems="center" justifyContent="end">
<ToggleThemeButton />
</HStack>
</div>
</div>
);
@@ -136,7 +135,9 @@ const _SidebarItem = forwardRef(function SidebarItem(
{ className, requestName, requestId, workspaceId, active }: SidebarItemProps,
ref: ForwardedRef<HTMLLIElement>,
) {
const latestResponse = useLatestResponse(requestId);
const updateRequest = useUpdateRequest(requestId);
const deleteRequest = useDeleteRequest(requestId);
const [editing, setEditing] = useState<boolean>(false);
const handleSubmitNameEdit = useCallback(
@@ -159,12 +160,17 @@ const _SidebarItem = forwardRef(function SidebarItem(
e.preventDefault();
setEditing(true);
}
if (active && (e.key === 'Backspace' || e.key === 'Delete')) {
e.preventDefault();
deleteRequest.mutate();
}
},
[active],
[active, deleteRequest],
);
const handleInputKeyDown = useCallback(
async (e: KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
switch (e.key) {
case 'Enter':
e.preventDefault();
@@ -183,21 +189,19 @@ const _SidebarItem = forwardRef(function SidebarItem(
<li ref={ref} className={classnames(className, 'block group/item px-2 pb-0.5')}>
<div className="relative">
<Button
tabIndex={0}
color="custom"
size="sm"
size="xs"
to={`/workspaces/${workspaceId}/requests/${requestId}`}
draggable={false} // Item should drag, not the link
onDoubleClick={() => setEditing(true)}
onClick={active ? () => setEditing(true) : undefined}
justify="start"
onKeyDown={handleKeyDown}
className={classnames(
editing && 'focus-within:border-focus',
editing && 'ring-1 focus-within:ring-focus',
active
? 'bg-highlight text-gray-900'
: 'text-gray-600 group-hover/item:text-gray-800 active:bg-highlightSecondary',
// Move out of the way when trash is shown
'group-hover/item:pr-7',
)}
>
{editing ? (
@@ -213,19 +217,16 @@ const _SidebarItem = forwardRef(function SidebarItem(
{requestName || 'New Request'}
</span>
)}
{latestResponse && (
<div className="ml-auto">
{isResponseLoading(latestResponse) ? (
<Icon spin size="sm" icon="update" />
) : (
<StatusTag className="text-2xs dark:opacity-80" response={latestResponse} />
)}
</div>
)}
</Button>
<RequestActionsDropdown requestId={requestId}>
<IconButton
color="custom"
size="sm"
title="Request Options"
icon="dotsH"
className={classnames(
'absolute right-0 top-0 transition-opacity !opacity-0',
'group-hover/item:!opacity-100 focus-visible:!opacity-100',
)}
/>
</RequestActionsDropdown>
</div>
</li>
);

View File

@@ -1,14 +1,26 @@
import { memo, useCallback } from 'react';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useCreateRequest } from '../hooks/useCreateRequest';
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useTauriEvent } from '../hooks/useTauriEvent';
import { IconButton } from './core/IconButton';
export const SidebarActions = memo(function SidebarDisplayToggle() {
const { hidden, toggle } = useSidebarHidden();
const activeRequestId = useActiveRequestId();
const createRequest = useCreateRequest({ navigateAfter: true });
const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true });
const handleCreateRequest = useCallback(() => {
createRequest.mutate({ name: 'New Request' });
createRequest.mutate({});
}, [createRequest]);
useTauriEvent('new_request', () => {
createRequest.mutate({});
});
// TODO: Put this somewhere better
useTauriEvent('duplicate_request', () => {
duplicateRequest.mutate();
});
return (
<>

View File

@@ -1,14 +0,0 @@
import React from 'react';
import { useTheme } from '../hooks/useTheme';
import { IconButton } from './core/IconButton';
export function ToggleThemeButton() {
const { appearance, toggleAppearance } = useTheme();
return (
<IconButton
title={appearance === 'dark' ? 'Enable light mode' : 'Enable dark mode'}
icon={appearance === 'dark' ? 'moon' : 'sun'}
onClick={toggleAppearance}
/>
);
}

View File

@@ -119,13 +119,6 @@ export default function Workspace() {
!isResizing && 'transition-all',
)}
>
<HeaderSize
data-tauri-drag-region
className="w-full bg-gray-50 border-b border-b-highlight text-gray-900"
style={head}
>
<WorkspaceHeader className="pointer-events-none" />
</HeaderSize>
{floating ? (
<Overlay open={!hidden} portalName="sidebar" onClose={hide}>
<motion.div
@@ -162,6 +155,13 @@ export default function Workspace() {
/>
</>
)}
<HeaderSize
data-tauri-drag-region
className="w-full bg-gray-50 border-b border-b-highlight text-gray-900"
style={head}
>
<WorkspaceHeader className="pointer-events-none" />
</HeaderSize>
<RequestResponse style={body} />
</div>
);

View File

@@ -1,7 +1,6 @@
import classnames from 'classnames';
import { memo, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useConfirm } from '../hooks/useConfirm';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
import { useRoutes } from '../hooks/useRoutes';
@@ -10,7 +9,6 @@ import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
type Props = {
className?: string;
@@ -21,9 +19,8 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = activeWorkspace?.id ?? null;
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
const deleteWorkspace = useDeleteWorkspace(activeWorkspaceId);
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
const routes = useRoutes();
const confirm = useConfirm();
const items: DropdownItem[] = useMemo(() => {
const workspaceItems = workspaces.map((w) => ({
@@ -49,20 +46,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
{
label: 'Delete Workspace',
leftSlot: <Icon icon="trash" />,
onSelect: async () => {
const confirmed = await confirm({
title: 'Delete Workspace',
variant: 'delete',
description: (
<>
Are you sure you want to delete <InlineCode>{activeWorkspace?.name}</InlineCode>?
</>
),
});
if (confirmed) {
deleteWorkspace.mutate();
}
},
onSelect: deleteWorkspace.mutate,
},
];
}, [

View File

@@ -13,6 +13,7 @@ interface Props {
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
const activeRequest = useActiveRequest();
return (
<HStack
justifyContent="center"

View File

@@ -46,7 +46,6 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
() =>
classnames(
className,
'opacity-90 hover:opacity-100',
'outline-none whitespace-nowrap',
'focus-visible-or-class:ring',
'rounded-md flex items-center',

View File

@@ -266,7 +266,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
onClick={handleClick}
className={classnames(
className,
'min-w-[8rem] outline-none px-2 mx-1.5 h-7 flex items-center text-sm text-gray-700 whitespace-nowrap pr-4',
'min-w-[8rem] outline-none px-2 mx-1.5 h-7 flex items-center text-sm text-gray-700 whitespace-nowrap',
'focus:bg-highlight focus:text-gray-900 rounded',
)}
{...props}

View File

@@ -39,7 +39,7 @@
/* Style gutters */
.cm-gutters {
@apply border-0 text-gray-500/50 opacity-95;
@apply border-0 text-gray-500/50;
.cm-gutterElement {
@apply cursor-default;
@@ -183,20 +183,16 @@
@apply bg-highlight text-gray-900;
}
& > ul > li:hover {
@apply text-gray-800;
}
.cm-completionIcon {
@apply text-sm flex items-center pb-0.5;
@apply text-sm flex items-center pb-0.5 flex-shrink-0;
}
.cm-completionLabel {
@apply text-gray-700;
}
.cm-completionDetail {
@apply ml-auto;
@apply ml-auto pl-6;
}
}
}

View File

@@ -35,6 +35,7 @@ export interface EditorProps {
onFocus?: () => void;
onBlur?: () => void;
singleLine?: boolean;
wrapLines?: boolean;
format?: (v: string) => string;
autocomplete?: GenericCompletionConfig;
actions?: ReactNode;
@@ -59,6 +60,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
format,
autocomplete,
actions,
wrapLines,
}: EditorProps,
ref,
) {
@@ -93,6 +95,15 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
cm.current?.view.dispatch({ effects: effect });
}, [placeholder]);
// Update wrap lines
const wrapLinesCompartment = useRef(new Compartment());
useEffect(() => {
if (cm.current === null) return;
const ext = wrapLines ? [EditorView.lineWrapping] : [];
const effect = wrapLinesCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects: effect });
}, [wrapLines]);
// Update language extension when contentType changes
useEffect(() => {
if (cm.current === null) return;
@@ -126,16 +137,15 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
doc: `${defaultValue ?? ''}`,
extensions: [
languageCompartment.of(langExt),
placeholderCompartment.current.of(
placeholderExt(placeholderElFromText(placeholder ?? '')),
),
placeholderCompartment.current.of([]),
wrapLinesCompartment.current.of([]),
...getExtensions({
container,
readOnly,
singleLine,
onChange: handleChange,
onFocus: handleFocus,
onBlur: handleBlur,
readOnly,
singleLine,
}),
],
});

View File

@@ -3,18 +3,8 @@ import type { CompletionContext } from '@codemirror/autocomplete';
const openTag = '${[ ';
const closeTag = ' ]}';
const variables = [
{ name: 'DOMAIN' },
{ name: 'BASE_URL' },
{ name: 'CONTENT_THINGY' },
{ name: 'TOKEN' },
{ name: 'PROJECT_ID' },
{ name: 'DUMMY' },
{ name: 'DUMMY_2' },
{ name: 'STRIPE_PUB_KEY' },
{ name: 'RAILWAY_TOKEN' },
{ name: 'SECRET' },
{ name: 'PORT' },
const variables: { name: string }[] = [
// TODO: Put variables here
];
const MIN_MATCH_VAR = 2;

View File

@@ -5,7 +5,7 @@ export function HotKey({ children }: HTMLAttributes<HTMLSpanElement>) {
return (
<span
className={classnames(
'bg-gray-400 bg-opacity-20 px-1.5 py-0.5 rounded text-sm',
'bg-highlightSecondary bg-opacity-20 px-1.5 py-0.5 rounded text-sm',
'font-mono text-gray-500 tracking-widest',
)}
>

View File

@@ -57,12 +57,12 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
const [focused, setFocused] = useState(false);
const handleOnFocus = useCallback(() => {
const handleFocus = useCallback(() => {
setFocused(true);
onFocus?.();
}, [onFocus]);
const handleOnBlur = useCallback(() => {
const handleBlur = useCallback(() => {
setFocused(false);
onBlur?.();
}, [onBlur]);
@@ -107,8 +107,8 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
className={classnames(
containerClassName,
'relative w-full rounded-md text-gray-900',
'border border-highlight',
focused && 'border-focus',
'border',
focused ? 'border-focus' : 'border-highlight',
!isValid && '!border-invalid',
size === 'md' && 'h-md leading-md',
size === 'sm' && 'h-sm leading-sm',
@@ -125,8 +125,8 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
placeholder={placeholder}
onChange={handleChange}
className={inputClassName}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onFocus={handleFocus}
onBlur={handleBlur}
{...props}
/>
{type === 'password' && (

View File

@@ -137,7 +137,7 @@ export const PairEditor = memo(function PairEditor({
className={classnames(
className,
'@container',
'pb-2 grid',
'pb-2 grid overflow-auto max-h-full',
// Move over the width of the drag handle
'-ml-3',
)}

View File

@@ -1,23 +0,0 @@
import classnames from 'classnames';
import type { ReactNode } from 'react';
interface Props {
statusCode: number;
children: ReactNode;
}
export function StatusColor({ statusCode, children }: Props) {
return (
<span
className={classnames(
statusCode >= 100 && statusCode < 200 && 'text-green-600',
statusCode >= 200 && statusCode < 300 && 'text-green-600',
statusCode >= 300 && statusCode < 400 && 'text-pink-600',
statusCode >= 400 && statusCode < 500 && 'text-orange-600',
statusCode >= 500 && statusCode < 600 && 'text-red-600',
)}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,28 @@
import classnames from 'classnames';
import type { HttpResponse } from '../../lib/models';
interface Props {
response: Pick<HttpResponse, 'status' | 'error'>;
className?: string;
}
export function StatusTag({ response, className }: Props) {
const { status, error } = response;
const label = error ? 'ERR' : status;
return (
<span
className={classnames(
className,
'font-mono',
status >= 0 && status < 100 && 'text-red-600',
status >= 100 && status < 200 && 'text-green-600',
status >= 200 && status < 300 && 'text-green-600',
status >= 300 && status < 400 && 'text-pink-600',
status >= 400 && status < 500 && 'text-orange-600',
status >= 500 && 'text-red-600',
)}
>
{label}
</span>
);
}

View File

@@ -81,8 +81,10 @@ export function Tabs({
<HStack space={1} className="flex-shrink-0">
{tabs.map((t) => {
const isActive = t.value === value;
// const btnClassName = classnames(isActive ? 'bg-highlightSecondary' : 'text-gray-600');
const btnClassName = classnames(isActive ? '' : 'text-gray-600', '!px-0 mr-4 ml-[1px]');
const btnClassName = classnames(
isActive ? '' : 'text-gray-600 hover:text-gray-800',
'!px-0 mr-4 ml-[1px]',
);
if ('options' in t) {
const option = t.options.items.find(
@@ -147,7 +149,7 @@ export const TabContent = memo(function TabContent({
<div
tabIndex={-1}
data-tab={value}
className={classnames(className, 'tab-content', 'overflow-auto hidden w-full h-full')}
className={classnames(className, 'tab-content', 'hidden w-full h-full')}
>
{children}
</div>

View File

@@ -16,12 +16,12 @@ export function Webview({ body, url, contentType }: Props) {
}, [url, body, contentType]);
return (
<div className="px-2 pb-2">
<div className="h-full pb-3">
<iframe
title="Response preview"
srcDoc={contentForIframe}
sandbox="allow-scripts allow-same-origin"
className="h-full w-full rounded-md border border-gray-100/20"
className="h-full w-full rounded border border-highlightSecondary"
/>
</div>
);

View File

@@ -16,8 +16,9 @@ export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean })
if (workspaceId === null) {
throw new Error("Cannot create request when there's no active workspace");
}
const sortPriority = maxSortPriority(requests) + 1000;
return invoke('create_request', { sortPriority, workspaceId, ...patch });
patch.name = patch.name || 'New Request';
patch.sortPriority = patch.sortPriority || maxSortPriority(requests) + 1000;
return invoke('create_request', { workspaceId, ...patch });
},
onSuccess: async (request) => {
queryClient.setQueryData<HttpRequest[]>(

View File

@@ -1,7 +1,10 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { InlineCode } from '../components/core/InlineCode';
import type { HttpRequest } from '../lib/models';
import { getRequest } from '../lib/store';
import { useActiveRequestId } from './useActiveRequestId';
import { useConfirm } from './useConfirm';
import { requestsQueryKey } from './useRequests';
import { responsesQueryKey } from './useResponses';
import { useRoutes } from './useRoutes';
@@ -10,9 +13,28 @@ export function useDeleteRequest(id: string | null) {
const queryClient = useQueryClient();
const activeRequestId = useActiveRequestId();
const routes = useRoutes();
return useMutation<HttpRequest, string>({
mutationFn: async () => invoke('delete_request', { requestId: id }),
onSuccess: async ({ workspaceId, id: requestId }) => {
const confirm = useConfirm();
return useMutation<HttpRequest | null, string>({
mutationFn: async () => {
const request = await getRequest(id);
const confirmed = await confirm({
title: 'Delete Request',
variant: 'delete',
description: (
<>
Are you sure you want to delete <InlineCode>{request?.name}</InlineCode>?
</>
),
});
if (!confirmed) return null;
return invoke('delete_request', { requestId: id });
},
onSuccess: async (request) => {
// Was it cancelled?
if (request === null) return;
const { workspaceId, id: requestId } = request;
queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId),

View File

@@ -1,20 +1,37 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { InlineCode } from '../components/core/InlineCode';
import type { Workspace } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useConfirm } from './useConfirm';
import { requestsQueryKey } from './useRequests';
import { useRoutes } from './useRoutes';
import { workspacesQueryKey } from './useWorkspaces';
export function useDeleteWorkspace(id: string | null) {
export function useDeleteWorkspace(workspace: Workspace | null) {
const queryClient = useQueryClient();
const activeWorkspaceId = useActiveWorkspaceId();
const routes = useRoutes();
return useMutation<Workspace, string>({
mutationFn: () => {
return invoke('delete_workspace', { id });
const confirm = useConfirm();
return useMutation<Workspace | null, string>({
mutationFn: async () => {
const confirmed = await confirm({
title: 'Delete Workspace',
variant: 'delete',
description: (
<>
Are you sure you want to delete <InlineCode>{workspace?.name}</InlineCode>?
</>
),
});
if (!confirmed) return null;
return invoke('delete_workspace', { id: workspace?.id });
},
onSuccess: async ({ id: workspaceId }) => {
onSuccess: async (workspace) => {
if (workspace === null) return;
const { id: workspaceId } = workspace;
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) =>
workspaces?.filter((workspace) => workspace.id !== workspaceId),
);

View File

@@ -1,8 +1,8 @@
import { useResponses } from './useResponses';
import { isResponseLoading } from '../lib/models';
import { useLatestResponse } from './useLatestResponse';
export function useIsResponseLoading(requestId: string | null): boolean {
const responses = useResponses(requestId);
const response = responses[responses.length - 1];
if (!response) return false;
return !(response.body || response.status || response.error);
const response = useLatestResponse(requestId);
if (response === null) return false;
return isResponseLoading(response);
}

View File

@@ -0,0 +1,7 @@
import type { HttpResponse } from '../lib/models';
import { useResponses } from './useResponses';
export function useLatestResponse(requestId: string | null): HttpResponse | null {
const responses = useResponses(requestId);
return responses[responses.length - 1] ?? null;
}

View File

@@ -1,17 +0,0 @@
import { getKeyValue, NAMESPACE_NO_SYNC, setKeyValue } from './keyValueStore';
export async function getLastLocation(): Promise<string> {
return getKeyValue({ namespace: NAMESPACE_NO_SYNC, key: 'last_location', fallback: '/' });
}
export async function setLastLocation(pathname: string): Promise<void> {
return setKeyValue({ namespace: NAMESPACE_NO_SYNC, key: 'last_location', value: pathname });
}
export async function syncLastLocation(): Promise<void> {
const lastPathname = await getLastLocation();
if (lastPathname !== window.location.pathname) {
console.log(`Redirecting to last location: ${lastPathname}`);
window.location.assign(lastPathname);
}
}

View File

@@ -72,3 +72,7 @@ export interface HttpResponse extends BaseModel {
readonly url: string;
readonly headers: HttpHeader[];
}
export function isResponseLoading(response: HttpResponse): boolean {
return !(response.body || response.status || response.error);
}

View File

@@ -12,11 +12,11 @@ const darkTheme: AppTheme = {
colors: {
gray: '#6b5b98',
red: '#ff417b',
orange: '#ff9411',
orange: '#fd9014',
yellow: '#e8d13f',
green: '#43e76f',
green: '#3fd265',
blue: '#219dff',
pink: '#f670f6',
pink: '#ff6dff',
violet: '#b176ff',
},
},
@@ -31,11 +31,11 @@ const lightTheme: AppTheme = {
colors: {
gray: '#7f8fb0',
red: '#ec3f87',
orange: '#ff8b00',
orange: '#ff8000',
yellow: '#e7cf24',
green: '#00d365',
blue: '#0090ff',
pink: '#f670f6',
pink: '#ea6cea',
violet: '#ac6cff',
},
},
@@ -50,14 +50,6 @@ export function getAppearance(): Appearance {
return getPreferredAppearance();
}
export function toggleAppearance(): Appearance {
const currentTheme =
document.documentElement.getAttribute('data-appearance') ?? getPreferredAppearance();
const newAppearance = currentTheme === 'dark' ? 'light' : 'dark';
setAppearance(newAppearance);
return newAppearance;
}
export function setAppearance(a?: Appearance) {
const appearance = a ?? getPreferredAppearance();
const theme = appearance === 'dark' ? darkTheme : lightTheme;

View File

@@ -2,12 +2,10 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './components/App';
import { getKeyValue } from './lib/keyValueStore';
import { syncLastLocation } from './lib/lastLocation';
import { getPreferredAppearance, setAppearance } from './lib/theme/window';
import './main.css';
setAppearance(await getKeyValue({ key: 'appearance', fallback: getPreferredAppearance() }));
await syncLastLocation();
// root holds our app's root DOM Element:
createRoot(document.getElementById('root') as HTMLElement).render(

View File

@@ -16,13 +16,13 @@ module.exports = {
"xs": "0.8rem"
},
height: {
"xs": "1.5rem",
"xs": "1.75rem",
"sm": "2.0rem",
"md": "2.5rem"
},
lineHeight: {
// HACK: Minus 2 to account for borders inside inputs
"xs": "calc(1.5rem - 2px)",
"xs": "calc(1.75rem - 2px)",
"sm": "calc(2.0rem - 2px)",
"md": "calc(2.5rem - 2px)"
},