mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:13:51 +01:00
Try new layout and a bunch of editor fixes
This commit is contained in:
@@ -1,19 +1,17 @@
|
||||
import classnames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Editor from './components/Editor/Editor';
|
||||
import { HStack, VStack } from './components/Stacks';
|
||||
import { WindowDragRegion } from './components/WindowDragRegion';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
import { UrlBar } from './components/UrlBar';
|
||||
import { Grid } from './components/Grid';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Grid } from './components/Grid';
|
||||
import { RequestPane } from './components/RequestPane';
|
||||
import { ResponsePane } from './components/ResponsePane';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
import { HStack } from './components/Stacks';
|
||||
import {
|
||||
useDeleteRequest,
|
||||
useRequests,
|
||||
useRequestUpdate,
|
||||
useSendRequest,
|
||||
} from './hooks/useRequest';
|
||||
import { ResponsePane } from './components/ResponsePane';
|
||||
import { IconButton } from './components/IconButton';
|
||||
|
||||
type Params = {
|
||||
workspaceId: string;
|
||||
@@ -26,56 +24,19 @@ function App() {
|
||||
const { data: requests } = useRequests(workspaceId);
|
||||
const request = requests?.find((r) => r.id === p.requestId);
|
||||
|
||||
const updateRequest = useRequestUpdate(request ?? null);
|
||||
const sendRequest = useSendRequest(request ?? null);
|
||||
const deleteRequest = useDeleteRequest(request ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = async (e: KeyboardEvent) => {
|
||||
if (e.metaKey && (e.key === 'Enter' || e.key === 'r')) {
|
||||
await sendRequest.mutate();
|
||||
}
|
||||
};
|
||||
document.documentElement.addEventListener('keypress', listener);
|
||||
return () => document.documentElement.removeEventListener('keypress', listener);
|
||||
}, []);
|
||||
|
||||
const [screenWidth, setScreenWidth] = useState(window.innerWidth);
|
||||
useEffect(() => {
|
||||
console.log('SCREEN WIDTH', document.documentElement.clientWidth);
|
||||
window.addEventListener('resize', () => setScreenWidth(window.innerWidth));
|
||||
}, []);
|
||||
const isH = screenWidth > 900;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[auto_1fr] h-full text-gray-900">
|
||||
<Sidebar requests={requests ?? []} workspaceId={workspaceId} activeRequestId={request?.id} />
|
||||
{request && (
|
||||
<Grid cols={screenWidth > 700 ? 2 : 1} rows={screenWidth > 700 ? 1 : 2}>
|
||||
<VStack className="w-full">
|
||||
<HStack as={WindowDragRegion} items="center" className="pl-3 pr-1.5">
|
||||
Test Request
|
||||
<IconButton size="sm" icon="trash" onClick={() => deleteRequest.mutate()} />
|
||||
</HStack>
|
||||
<VStack className="pl-3 px-1.5 py-3" space={3}>
|
||||
<UrlBar
|
||||
key={request.id}
|
||||
method={request.method}
|
||||
url={request.url}
|
||||
loading={sendRequest.isLoading}
|
||||
onMethodChange={(method) => updateRequest.mutate({ method })}
|
||||
onUrlChange={(url) => updateRequest.mutate({ url })}
|
||||
sendRequest={sendRequest.mutate}
|
||||
/>
|
||||
<Editor
|
||||
valueKey={request.id}
|
||||
useTemplating
|
||||
defaultValue={request.body ?? ''}
|
||||
contentType="application/json"
|
||||
onChange={(body) => updateRequest.mutate({ body })}
|
||||
/>
|
||||
</VStack>
|
||||
</VStack>
|
||||
<ResponsePane requestId={request.id} error={sendRequest.error} />
|
||||
<Grid cols={isH ? 2 : 1} rows={isH ? 1 : 2} gap={2}>
|
||||
<RequestPane request={request} className={classnames(isH ? 'pr-0' : 'pb-0')} />
|
||||
<ResponsePane requestId={request.id} className={classnames(isH ? 'pl-0' : 'pt-0')} />
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,14 +8,22 @@ import type {
|
||||
import { forwardRef } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export interface ButtonProps<T extends ElementType>
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
color?: 'primary' | 'secondary' | 'warning' | 'danger';
|
||||
const colorStyles = {
|
||||
default: 'hover:bg-gray-500/10 text-gray-600',
|
||||
gray: 'bg-gray-50 text-gray-800 hover:bg-gray-500/10',
|
||||
primary: 'bg-blue-400',
|
||||
secondary: 'bg-violet-400',
|
||||
warning: 'bg-orange-400',
|
||||
danger: 'bg-red-400',
|
||||
};
|
||||
|
||||
export type ButtonProps<T extends ElementType> = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
color?: keyof typeof colorStyles;
|
||||
size?: 'xs' | 'sm' | 'md';
|
||||
justify?: 'start' | 'center';
|
||||
forDropdown?: boolean;
|
||||
as?: T;
|
||||
}
|
||||
};
|
||||
|
||||
export const Button = forwardRef(function Button<T extends ElementType>(
|
||||
{
|
||||
@@ -37,18 +45,14 @@ export const Button = forwardRef(function Button<T extends ElementType>(
|
||||
type="button"
|
||||
className={classnames(
|
||||
className,
|
||||
'rounded-md flex items-center bg-opacity-80 hover:bg-opacity-100 text-white',
|
||||
'transition-all rounded-md flex items-center bg-opacity-80 hover:bg-opacity-100 hover:text-white',
|
||||
// 'active:translate-y-[0.5px] active:scale-[0.99]',
|
||||
colorStyles[color || 'default'],
|
||||
justify === 'start' && 'justify-start',
|
||||
justify === 'center' && 'justify-center',
|
||||
size === 'md' && 'h-10 px-4',
|
||||
size === 'sm' && 'h-8 px-3 text-sm',
|
||||
size === 'xs' && 'h-7 px-3 text-sm',
|
||||
color === undefined && 'hover:bg-gray-500/[0.1]',
|
||||
color === 'primary' && 'bg-blue-400',
|
||||
color === 'secondary' && 'bg-violet-400',
|
||||
color === 'warning' && 'bg-orange-400',
|
||||
color === 'danger' && 'bg-red-400',
|
||||
size === 'xs' && 'h-6 px-3 text-xs',
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -29,25 +29,27 @@ export function Dialog({
|
||||
<D.Portal container={document.querySelector<HTMLElement>('#radix-portal')}>
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
|
||||
<D.Overlay className="fixed inset-0 bg-gray-900 dark:bg-background opacity-80 shadow-lg" />
|
||||
<D.Content className={classnames(className, 'dialog-content', 'fixed inset-0')}>
|
||||
<div
|
||||
className={classnames(
|
||||
className,
|
||||
'absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] bg-gray-50',
|
||||
'w-[20rem] max-h-[80vh] p-5 rounded-lg overflow-auto',
|
||||
wide && 'w-[80vw] max-w-[50rem]',
|
||||
)}
|
||||
>
|
||||
<D.Close asChild className="ml-auto absolute right-1 top-1">
|
||||
<IconButton aria-label="Close" icon="x" size="sm" />
|
||||
</D.Close>
|
||||
<VStack space={3}>
|
||||
<HStack items="center" className="pb-3">
|
||||
<D.Title className="text-xl font-semibold">{title}</D.Title>
|
||||
</HStack>
|
||||
{description && <D.Description>{description}</D.Description>}
|
||||
<div>{children}</div>
|
||||
</VStack>
|
||||
<D.Content>
|
||||
<div className={classnames(className, 'fixed inset-0 pointer-events-none')}>
|
||||
<div
|
||||
className={classnames(
|
||||
className,
|
||||
'absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] bg-gray-50',
|
||||
'w-[20rem] max-h-[80vh] p-5 rounded-lg overflow-auto',
|
||||
wide && 'w-[80vw] max-w-[50rem]',
|
||||
)}
|
||||
>
|
||||
<D.Close asChild className="ml-auto absolute right-1 top-1">
|
||||
<IconButton aria-label="Close" icon="x" size="sm" />
|
||||
</D.Close>
|
||||
<VStack space={3}>
|
||||
<HStack items="center" className="pb-3">
|
||||
<D.Title className="text-xl font-semibold">{title}</D.Title>
|
||||
</HStack>
|
||||
{description && <D.Description>{description}</D.Description>}
|
||||
<div>{children}</div>
|
||||
</VStack>
|
||||
</div>
|
||||
</div>
|
||||
</D.Content>
|
||||
</motion.div>
|
||||
|
||||
23
src-web/components/Divider.tsx
Normal file
23
src-web/components/Divider.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as Separator from '@radix-ui/react-separator';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
decorative?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Divider({ className, orientation = 'horizontal', decorative }: Props) {
|
||||
return (
|
||||
<Separator.Root
|
||||
className={classnames(
|
||||
className,
|
||||
'bg-gray-50',
|
||||
orientation === 'horizontal' && 'w-full h-[1px]',
|
||||
orientation === 'vertical' && 'h-full w-[1px]',
|
||||
)}
|
||||
orientation={orientation}
|
||||
decorative={decorative}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -264,7 +264,11 @@ function DropdownMenuSeparator({ className, ...props }: D.DropdownMenuSeparatorP
|
||||
|
||||
function DropdownMenuTrigger({ children, className, ...props }: D.DropdownMenuTriggerProps) {
|
||||
return (
|
||||
<D.Trigger asChild className={classnames(className, 'focus:outline-none')} {...props}>
|
||||
<D.Trigger
|
||||
asChild
|
||||
className={classnames(className, 'focus:outline-none focus:border-0 focus:shadow-none')}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</D.Trigger>
|
||||
);
|
||||
|
||||
@@ -5,45 +5,62 @@
|
||||
}
|
||||
|
||||
.cm-wrapper .cm-editor {
|
||||
@apply inset-0;
|
||||
position: absolute !important;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
@apply w-full block;
|
||||
|
||||
&.cm-focused {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
@apply text-gray-900 pl-1 pr-1.5;
|
||||
}
|
||||
|
||||
.cm-placeholder {
|
||||
@apply text-placeholder;
|
||||
}
|
||||
|
||||
.placeholder-widget {
|
||||
@apply text-xs text-white/90 bg-blue-400/80 py-[1px] px-1 mx-[1px] rounded cursor-default hover:bg-blue-400 hover:text-white;
|
||||
text-shadow: 0 0 1px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.cm-singleline .cm-scroller {
|
||||
overflow: hidden !important;;
|
||||
|
||||
.cm-singleline {
|
||||
.cm-editor {
|
||||
@apply h-full w-full;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
font-family: inherit;
|
||||
overflow: hidden !important;;
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
@apply px-0;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor .placeholder-widget {
|
||||
@apply text-xs text-white bg-blue-400 py-[1px] px-1 mx-[1px] rounded cursor-default hover:bg-blue-500;
|
||||
text-shadow: 0 0 1px rgba(0, 0, 0, 0.9);
|
||||
.cm-multiline {
|
||||
.cm-editor {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
@apply rounded;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-multiline .cm-editor .cm-scroller {
|
||||
@apply rounded-lg bg-gray-50;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.cm-multiline .cm-editor.cm-focused .cm-scroller {
|
||||
box-shadow: 0 0 0 1px hsl(var(--color-blue-400)/0.4);
|
||||
}
|
||||
|
||||
.cm-editor .cm-line {
|
||||
color: hsl(var(--color-gray-900));
|
||||
}
|
||||
|
||||
.cm-multiline .cm-editor .cm-line {
|
||||
padding-left: 1em;
|
||||
padding-right: 1.5em;
|
||||
/* Active border state if we want it */
|
||||
/*box-shadow: 0 0 0 1px hsl(var(--color-blue-400)/0.4);*/
|
||||
}
|
||||
|
||||
.cm-singleline .cm-editor .cm-scroller {
|
||||
@@ -52,7 +69,8 @@
|
||||
}
|
||||
|
||||
.cm-editor .cm-gutters {
|
||||
@apply bg-gray-50 border-r-0 text-gray-200;
|
||||
/*@apply bg-gray-50 border-r-0 text-gray-200;*/
|
||||
@apply bg-transparent border-0 text-gray-200;
|
||||
}
|
||||
|
||||
.cm-editor .cm-gutterElement {
|
||||
@@ -113,39 +131,56 @@
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
|
||||
/* --> Add padding to container. For some reason, using padding on both adds an extra
|
||||
* 1px offset so we need to use a combination of padding and margin.
|
||||
*/
|
||||
.cm-editor .cm-gutters {
|
||||
@apply pt-1;
|
||||
.cm-singleline .cm-editor {
|
||||
.cm-content {
|
||||
@apply h-full flex items-center;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor .cm-content {
|
||||
@apply mt-1;
|
||||
.cm-scroller {
|
||||
&::-webkit-scrollbar-corner,
|
||||
&::-webkit-scrollbar {
|
||||
@apply w-[5px] h-[5px] bg-transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-100 bg-opacity-30 rounded-full;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused .cm-scroller::-webkit-scrollbar-thumb {
|
||||
@apply bg-opacity-80;
|
||||
}
|
||||
|
||||
/* <-- */
|
||||
|
||||
/* NOTE: Extra selector required to override default styles */
|
||||
.cm-tooltip.cm-tooltip {
|
||||
@apply shadow-lg bg-background rounded overflow-hidden text-gray-900 border border-gray-100/70 z-50;
|
||||
}
|
||||
@apply shadow-lg bg-background rounded overflow-hidden text-gray-900 border border-gray-100/70 z-50 pointer-events-auto;
|
||||
|
||||
.cm-tooltip.cm-tooltip * {
|
||||
@apply transition-none;
|
||||
}
|
||||
* {
|
||||
@apply transition-none;
|
||||
}
|
||||
|
||||
.cm-tooltip.cm-tooltip.cm-tooltip-autocomplete > ul {
|
||||
@apply p-1 max-h-[40vh];
|
||||
}
|
||||
&.cm-tooltip-autocomplete {
|
||||
& > ul {
|
||||
@apply p-1 max-h-[40vh];
|
||||
}
|
||||
|
||||
.cm-tooltip.cm-tooltip.cm-tooltip-autocomplete > ul > li {
|
||||
@apply cursor-default py-1 px-2 rounded-sm text-gray-500;
|
||||
}
|
||||
& > ul > li {
|
||||
@apply cursor-default px-2 rounded-sm text-gray-500 h-7 flex items-center;
|
||||
}
|
||||
|
||||
.cm-tooltip.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected] {
|
||||
@apply bg-gray-50 text-gray-800;
|
||||
}
|
||||
& > ul > li[aria-selected] {
|
||||
@apply bg-gray-50 text-gray-800;
|
||||
}
|
||||
|
||||
.cm-tooltip.cm-tooltip.cm-tooltip-autocomplete .cm-completionIcon {
|
||||
@apply text-sm;
|
||||
& > ul > li:hover {
|
||||
@apply text-gray-700;
|
||||
}
|
||||
|
||||
.cm-completionIcon {
|
||||
@apply text-sm flex items-center pb-0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
||||
import classnames from 'classnames';
|
||||
@@ -9,29 +10,30 @@ import './Editor.css';
|
||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||
import { singleLineExt } from './singleLine';
|
||||
|
||||
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||
contentType: string;
|
||||
valueKey?: string;
|
||||
export interface EditorProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||
contentType?: string;
|
||||
autoFocus?: boolean;
|
||||
valueKey?: string | number;
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
tooltipContainer?: HTMLElement;
|
||||
useTemplating?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
onSubmit?: () => void;
|
||||
singleLine?: boolean;
|
||||
}
|
||||
|
||||
export default function Editor({
|
||||
contentType,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
valueKey,
|
||||
useTemplating,
|
||||
defaultValue,
|
||||
onChange,
|
||||
onSubmit,
|
||||
className,
|
||||
singleLine,
|
||||
...props
|
||||
}: Props) {
|
||||
}: EditorProps) {
|
||||
const [cm, setCm] = useState<{ view: EditorView; langHolder: Compartment } | null>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const extensions = useMemo(
|
||||
@@ -39,7 +41,6 @@ export default function Editor({
|
||||
getExtensions({
|
||||
container: ref.current,
|
||||
placeholder,
|
||||
onSubmit,
|
||||
singleLine,
|
||||
onChange,
|
||||
contentType,
|
||||
@@ -48,25 +49,23 @@ export default function Editor({
|
||||
[contentType, ref.current],
|
||||
);
|
||||
|
||||
const newState = (langHolder: Compartment) => {
|
||||
const langExt = getLanguageExtension({ contentType, useTemplating });
|
||||
return EditorState.create({
|
||||
doc: `${defaultValue ?? ''}`,
|
||||
extensions: [...extensions, langHolder.of(langExt)],
|
||||
});
|
||||
};
|
||||
|
||||
// Create codemirror instance when ref initializes
|
||||
useEffect(() => {
|
||||
if (ref.current === null) return;
|
||||
let view: EditorView | null = null;
|
||||
try {
|
||||
const langHolder = new Compartment();
|
||||
const langExt = getLanguageExtension({ contentType, useTemplating });
|
||||
const state = EditorState.create({
|
||||
doc: `${defaultValue ?? ''}`,
|
||||
extensions: [...extensions, langHolder.of(langExt)],
|
||||
});
|
||||
view = new EditorView({
|
||||
state: newState(langHolder),
|
||||
state,
|
||||
parent: ref.current,
|
||||
});
|
||||
setCm({ view, langHolder });
|
||||
if (autoFocus && view) view.focus();
|
||||
} catch (e) {
|
||||
console.log('Failed to initialize Codemirror', e);
|
||||
}
|
||||
@@ -108,17 +107,19 @@ function getExtensions({
|
||||
singleLine,
|
||||
placeholder,
|
||||
onChange,
|
||||
onSubmit,
|
||||
contentType,
|
||||
useTemplating,
|
||||
}: Pick<
|
||||
Props,
|
||||
'singleLine' | 'onChange' | 'onSubmit' | 'contentType' | 'useTemplating' | 'placeholder'
|
||||
EditorProps,
|
||||
'singleLine' | 'onChange' | 'contentType' | 'useTemplating' | 'placeholder'
|
||||
> & { container: HTMLDivElement | null }) {
|
||||
const ext = getLanguageExtension({ contentType, useTemplating });
|
||||
|
||||
// TODO: This is a hack to get the tooltips to render in the correct place when inside a modal dialog
|
||||
const parent = container?.closest<HTMLDivElement>('.dialog-content') ?? undefined;
|
||||
// TODO: Ensure tooltips render inside the dialog if we are in one.
|
||||
const parent =
|
||||
container?.closest<HTMLDivElement>('[role="dialog"]') ??
|
||||
document.querySelector<HTMLDivElement>('#cm-portal') ??
|
||||
undefined;
|
||||
|
||||
return [
|
||||
...baseExtensions,
|
||||
@@ -130,11 +131,15 @@ function getExtensions({
|
||||
...(placeholder ? [placeholderExt(placeholder)] : []),
|
||||
|
||||
// Handle onSubmit
|
||||
...(onSubmit
|
||||
...(singleLine
|
||||
? [
|
||||
EditorView.domEventHandlers({
|
||||
keydown: (e) => {
|
||||
if (e.key === 'Enter') onSubmit?.();
|
||||
if (e.key === 'Enter') {
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
const form = el.closest('form');
|
||||
form?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
||||
}
|
||||
},
|
||||
}),
|
||||
]
|
||||
@@ -147,3 +152,24 @@ function getExtensions({
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
const newState = ({
|
||||
langHolder,
|
||||
contentType,
|
||||
useTemplating,
|
||||
defaultValue,
|
||||
extensions,
|
||||
}: {
|
||||
langHolder: Compartment;
|
||||
contentType?: string;
|
||||
useTemplating?: boolean;
|
||||
defaultValue?: string;
|
||||
extensions: Extension[];
|
||||
}) => {
|
||||
console.log('NEW STATE', defaultValue);
|
||||
const langExt = getLanguageExtension({ contentType, useTemplating });
|
||||
return EditorState.create({
|
||||
doc: `${defaultValue ?? ''}`,
|
||||
extensions: [...extensions, langHolder.of(langExt)],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
keymap,
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
tooltips,
|
||||
} from '@codemirror/view';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { twig } from './twig/extension';
|
||||
@@ -90,10 +89,10 @@ export function getLanguageExtension({
|
||||
contentType,
|
||||
useTemplating,
|
||||
}: {
|
||||
contentType: string;
|
||||
contentType?: string;
|
||||
useTemplating?: boolean;
|
||||
}) {
|
||||
const justContentType = contentType.split(';')[0] ?? contentType;
|
||||
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
|
||||
const base = syntaxExtensions[justContentType] ?? json();
|
||||
if (!useTemplating) {
|
||||
return [base];
|
||||
@@ -108,7 +107,7 @@ export const baseExtensions = [
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
autocompletion({ closeOnBlur: true }),
|
||||
autocompletion({ closeOnBlur: true, interactionDelay: 200 }),
|
||||
syntaxHighlighting(myHighlightStyle),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { CompletionContext } from '@codemirror/autocomplete';
|
||||
import { match } from 'assert';
|
||||
|
||||
const openTag = '${[ ';
|
||||
const closeTag = ' ]}';
|
||||
@@ -18,7 +17,7 @@ const variables = [
|
||||
];
|
||||
|
||||
const MIN_MATCH_VAR = 2;
|
||||
const MIN_MATCH_NAME = 2;
|
||||
const MIN_MATCH_NAME = 4;
|
||||
|
||||
export function completions(context: CompletionContext) {
|
||||
const toStartOfName = context.matchBefore(/\w*/);
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import classnames from 'classnames';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
const colsClasses = {
|
||||
none: 'grid-cols-none',
|
||||
1: 'grid-cols-1',
|
||||
2: 'grid-cols-2',
|
||||
3: 'grid-cols-2',
|
||||
};
|
||||
|
||||
const rowsClasses = {
|
||||
none: 'grid-rows-none',
|
||||
1: 'grid-rows-1',
|
||||
2: 'grid-rows-2',
|
||||
3: 'grid-rows-2',
|
||||
};
|
||||
|
||||
const gapClasses = {
|
||||
0: 'gap-0',
|
||||
1: 'gap-1',
|
||||
2: 'gap-2',
|
||||
3: 'gap-3',
|
||||
};
|
||||
|
||||
type Props = HTMLAttributes<HTMLElement> & {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FormEvent } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { HttpHeader } from '../lib/models';
|
||||
import { IconButton } from './IconButton';
|
||||
import { Input } from './Input';
|
||||
@@ -7,82 +7,107 @@ import { HStack, VStack } from './Stacks';
|
||||
|
||||
export function HeaderEditor() {
|
||||
const [headers, setHeaders] = useState<HttpHeader[]>([]);
|
||||
const [newHeader, setNewHeader] = useState<HttpHeader>({ name: '', value: '' });
|
||||
const handleSubmit = (e?: FormEvent) => {
|
||||
console.log('SUBMIT');
|
||||
e?.preventDefault();
|
||||
setHeaders([...headers, newHeader]);
|
||||
setNewHeader({ name: '', value: '' });
|
||||
};
|
||||
const [newHeaderName, setNewHeaderName] = useState<string>('');
|
||||
const [newHeaderValue, setNewHeaderValue] = useState<string>('');
|
||||
const handleSubmit = useCallback(
|
||||
(e?: FormEvent) => {
|
||||
e?.preventDefault();
|
||||
setHeaders([...headers, { name: newHeaderName, value: newHeaderValue }]);
|
||||
setNewHeaderName('');
|
||||
setNewHeaderValue('');
|
||||
},
|
||||
[newHeaderName, newHeaderValue],
|
||||
);
|
||||
|
||||
const handleChangeHeader = useCallback(
|
||||
(header: Partial<HttpHeader>, index: number) => {
|
||||
setHeaders((headers) =>
|
||||
headers.map((h, i) => {
|
||||
if (i === index) return h;
|
||||
const newHeader: HttpHeader = { ...h, ...header };
|
||||
console.log('NEW HEADER', newHeader);
|
||||
return newHeader;
|
||||
}),
|
||||
);
|
||||
},
|
||||
[headers],
|
||||
);
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
setHeaders((headers) => headers.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleChangeHeader = (header: HttpHeader, index: number) => {
|
||||
setHeaders((headers) => headers.map((h, i) => (i === index ? header : h)));
|
||||
};
|
||||
console.log('HEADERS', headers);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<VStack space={2}>
|
||||
{headers.map((header, i) => (
|
||||
<FormRow
|
||||
key={`${headers.length}:${i}`}
|
||||
header={header}
|
||||
onChange={(h) => handleChangeHeader(h, i)}
|
||||
key={`${headers.length}-${i}`}
|
||||
valueKey={`${headers.length}-${i}`}
|
||||
name={header.name}
|
||||
value={header.value}
|
||||
onChangeName={(name) => handleChangeHeader({ name }, i)}
|
||||
onChangeValue={(value) => handleChangeHeader({ value }, i)}
|
||||
onDelete={() => handleDelete(i)}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
))}
|
||||
<FormRow addSubmit onChange={setNewHeader} header={newHeader} onSubmit={handleSubmit} />
|
||||
<FormRow
|
||||
autoFocus
|
||||
addSubmit
|
||||
valueKey={headers.length}
|
||||
onChangeName={setNewHeaderName}
|
||||
onChangeValue={setNewHeaderValue}
|
||||
name={newHeaderName}
|
||||
value={newHeaderValue}
|
||||
/>
|
||||
</VStack>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function FormRow({
|
||||
header,
|
||||
autoFocus,
|
||||
valueKey,
|
||||
name,
|
||||
value,
|
||||
addSubmit,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onChangeName,
|
||||
onChangeValue,
|
||||
onDelete,
|
||||
}: {
|
||||
header: HttpHeader;
|
||||
autoFocus?: boolean;
|
||||
valueKey: string | number;
|
||||
name: string;
|
||||
value: string;
|
||||
addSubmit?: boolean;
|
||||
onSubmit?: () => void;
|
||||
onChange: (header: HttpHeader) => void;
|
||||
onChangeName: (name: string) => void;
|
||||
onChangeValue: (value: string) => void;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<HStack space={2}>
|
||||
<Input
|
||||
autoFocus
|
||||
useEditor
|
||||
useTemplating
|
||||
hideLabel
|
||||
autoFocus={autoFocus}
|
||||
useEditor={{ useTemplating: true, valueKey }}
|
||||
name="name"
|
||||
label="Name"
|
||||
placeholder="name"
|
||||
onSubmit={onSubmit}
|
||||
value={header.name}
|
||||
hideLabel
|
||||
onChange={(name) => {
|
||||
onChange({ name, value: header.value });
|
||||
}}
|
||||
defaultValue={name}
|
||||
onChange={onChangeName}
|
||||
/>
|
||||
<Input
|
||||
hideLabel
|
||||
name="value"
|
||||
label="Value"
|
||||
useEditor
|
||||
useTemplating
|
||||
onSubmit={onSubmit}
|
||||
useEditor={{ useTemplating: true, valueKey }}
|
||||
placeholder="value"
|
||||
value={header.value}
|
||||
hideLabel
|
||||
onChange={(value) => {
|
||||
onChange({ name: header.name, value });
|
||||
}}
|
||||
defaultValue={value}
|
||||
onChange={onChangeValue}
|
||||
/>
|
||||
{onDelete && <IconButton size="sm" icon="trash" onClick={onDelete} />}
|
||||
</HStack>
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
CameraIcon,
|
||||
CheckIcon,
|
||||
CodeIcon,
|
||||
Cross1Icon,
|
||||
Cross2Icon,
|
||||
EyeOpenIcon,
|
||||
GearIcon,
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import classnames from 'classnames';
|
||||
import type { InputHTMLAttributes, ReactNode } from 'react';
|
||||
import type { EditorProps } from './Editor/Editor';
|
||||
import Editor from './Editor/Editor';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
|
||||
interface Props extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'onChange'> {
|
||||
interface Props
|
||||
extends Omit<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
'size' | 'onChange' | 'onSubmit' | 'defaultValue'
|
||||
> {
|
||||
name: string;
|
||||
label: string;
|
||||
hideLabel?: boolean;
|
||||
labelClassName?: string;
|
||||
containerClassName?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onSubmit?: () => void;
|
||||
contentType?: string;
|
||||
useTemplating?: boolean;
|
||||
useEditor?: boolean;
|
||||
useEditor?: Pick<EditorProps, 'contentType' | 'useTemplating' | 'valueKey'>;
|
||||
defaultValue?: string;
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
size?: 'sm' | 'md';
|
||||
@@ -25,13 +28,10 @@ export function Input({
|
||||
className,
|
||||
containerClassName,
|
||||
labelClassName,
|
||||
onSubmit,
|
||||
onChange,
|
||||
placeholder,
|
||||
useTemplating,
|
||||
size = 'md',
|
||||
useEditor,
|
||||
contentType,
|
||||
onChange,
|
||||
name,
|
||||
leftSlot,
|
||||
rightSlot,
|
||||
@@ -55,7 +55,7 @@ export function Input({
|
||||
items="center"
|
||||
className={classnames(
|
||||
containerClassName,
|
||||
'relative w-full rounded-md overflow-hidden text-gray-900 bg-gray-200/10',
|
||||
'relative w-full rounded-md text-gray-900 bg-gray-200/10',
|
||||
'border border-gray-500/10 focus-within:border-blue-400/40',
|
||||
size === 'md' && 'h-10',
|
||||
size === 'sm' && 'h-8',
|
||||
@@ -66,13 +66,15 @@ export function Input({
|
||||
<Editor
|
||||
id={id}
|
||||
singleLine
|
||||
contentType={contentType ?? 'text/plain'}
|
||||
useTemplating={useTemplating}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
onChange={onChange}
|
||||
className={classnames(
|
||||
className,
|
||||
'!bg-transparent min-w-0 pl-3 pr-2 h-full w-full focus:outline-none',
|
||||
)}
|
||||
{...props}
|
||||
{...useEditor}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
@@ -82,7 +84,7 @@ export function Input({
|
||||
defaultValue={defaultValue}
|
||||
className={classnames(
|
||||
className,
|
||||
'!bg-transparent min-w-0 pl-3 pr-2 h-full w-full focus:outline-none placeholder:text-gray-500/40',
|
||||
'!bg-transparent min-w-0 pl-3 pr-2 h-full w-full focus:outline-none placeholder:text-placeholder',
|
||||
leftSlot && '!pl-1',
|
||||
rightSlot && '!pr-1',
|
||||
)}
|
||||
|
||||
15
src-web/components/LayoutPane.tsx
Normal file
15
src-web/components/LayoutPane.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface LayoutPaneProps {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LayoutPane({ className, children }: LayoutPaneProps) {
|
||||
return (
|
||||
<div className={classnames(className, 'w-full h-full p-2')} data-tauri-drag-region>
|
||||
<div className={classnames('w-full h-full bg-gray-50/50 rounded-lg')}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
69
src-web/components/RequestPane.tsx
Normal file
69
src-web/components/RequestPane.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import classnames from 'classnames';
|
||||
import { useDeleteRequest, useRequestUpdate, useSendRequest } from '../hooks/useRequest';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { Button } from './Button';
|
||||
import { Divider } from './Divider';
|
||||
import Editor from './Editor/Editor';
|
||||
import type { LayoutPaneProps } from './LayoutPane';
|
||||
import { LayoutPane } from './LayoutPane';
|
||||
import { ScrollArea } from './ScrollArea';
|
||||
import { HStack } from './Stacks';
|
||||
import { UrlBar } from './UrlBar';
|
||||
|
||||
interface Props extends LayoutPaneProps {
|
||||
request: HttpRequest;
|
||||
}
|
||||
|
||||
export function RequestPane({ request, ...props }: Props) {
|
||||
const updateRequest = useRequestUpdate(request ?? null);
|
||||
const sendRequest = useSendRequest(request ?? null);
|
||||
return (
|
||||
<LayoutPane {...props}>
|
||||
<div className="h-full grid grid-rows-[auto_auto_minmax(0,1fr)] grid-cols-1 pt-1 pb-2">
|
||||
{/*<HStack as={WindowDragRegion} items="center" className="pl-3 pr-1.5">*/}
|
||||
{/* Test Request*/}
|
||||
{/* <IconButton size="sm" icon="trash" onClick={() => deleteRequest.mutate()} />*/}
|
||||
{/*</HStack>*/}
|
||||
<div>
|
||||
<UrlBar
|
||||
className="bg-transparent border-0 mb-1"
|
||||
key={request.id}
|
||||
method={request.method}
|
||||
url={request.url}
|
||||
loading={sendRequest.isLoading}
|
||||
onMethodChange={(method) => updateRequest.mutate({ method })}
|
||||
onUrlChange={(url) => updateRequest.mutate({ url })}
|
||||
sendRequest={sendRequest.mutate}
|
||||
/>
|
||||
<div className="mx-2">
|
||||
<Divider />
|
||||
</div>
|
||||
</div>
|
||||
{/*<Divider className="mb-2" />*/}
|
||||
<ScrollArea className="max-w-full pb-2 mx-2">
|
||||
<HStack className="mt-2 hide-scrollbar" space={1}>
|
||||
{['JSON', 'Params', 'Headers', 'Auth', 'Docs'].map((label, i) => (
|
||||
<Button
|
||||
key={label}
|
||||
size="xs"
|
||||
color={i === 0 && 'gray'}
|
||||
className={i !== 0 && 'opacity-50 hover:opacity-60'}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</HStack>
|
||||
</ScrollArea>
|
||||
<div className="px-0">
|
||||
<Editor
|
||||
valueKey={request.id}
|
||||
useTemplating
|
||||
defaultValue={request.body ?? ''}
|
||||
contentType="application/json"
|
||||
onChange={(body) => updateRequest.mutate({ body })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutPane>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import classnames from 'classnames';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useDeleteAllResponses, useDeleteResponse, useResponses } from '../hooks/useResponses';
|
||||
import { Divider } from './Divider';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import Editor from './Editor/Editor';
|
||||
import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
import { WindowDragRegion } from './WindowDragRegion';
|
||||
import type { LayoutPaneProps } from './LayoutPane';
|
||||
import { LayoutPane } from './LayoutPane';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
interface Props extends LayoutPaneProps {
|
||||
requestId: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function ResponsePane({ requestId, error }: Props) {
|
||||
export function ResponsePane({ requestId, className, ...props }: Props) {
|
||||
const [activeResponseId, setActiveResponseId] = useState<string | null>(null);
|
||||
const [viewMode, setViewMode] = useState<'pretty' | 'raw'>('pretty');
|
||||
const responses = useResponses(requestId);
|
||||
@@ -22,7 +23,6 @@ export function ResponsePane({ requestId, error }: Props) {
|
||||
: responses.data[responses.data.length - 1];
|
||||
const deleteResponse = useDeleteResponse(response);
|
||||
const deleteAllResponses = useDeleteAllResponses(response?.requestId);
|
||||
error = response?.error ?? error;
|
||||
|
||||
useEffect(() => {
|
||||
setActiveResponseId(null);
|
||||
@@ -44,49 +44,29 @@ export function ResponsePane({ requestId, error }: Props) {
|
||||
}, [response?.body, contentType]);
|
||||
|
||||
return (
|
||||
<VStack className="w-full">
|
||||
<HStack as={WindowDragRegion} items="center" className="pl-1.5 pr-1">
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: 'Clear Response',
|
||||
onSelect: deleteResponse.mutate,
|
||||
disabled: responses.data.length === 0,
|
||||
},
|
||||
{
|
||||
label: 'Clear All Responses',
|
||||
onSelect: deleteAllResponses.mutate,
|
||||
disabled: responses.data.length === 0,
|
||||
},
|
||||
'-----',
|
||||
...responses.data.slice(0, 10).map((r) => ({
|
||||
label: r.status + ' - ' + r.elapsed + ' ms',
|
||||
leftSlot: response?.id === r.id ? <Icon icon="check" /> : <></>,
|
||||
onSelect: () => setActiveResponseId(r.id),
|
||||
})),
|
||||
]}
|
||||
>
|
||||
<IconButton icon="gear" className="ml-auto" size="sm" />
|
||||
</Dropdown>
|
||||
</HStack>
|
||||
<motion.div animate={{ opacity: 1 }} initial={{ opacity: 0 }} className="w-full h-full">
|
||||
<VStack className="pr-3 pl-1.5 py-3" space={3}>
|
||||
{error && <div className="text-white bg-red-500 px-3 py-1 rounded">{error}</div>}
|
||||
{response && (
|
||||
<>
|
||||
<LayoutPane className={classnames(className)} {...props}>
|
||||
<div className="max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 py-1 px-2">
|
||||
{/*<HStack as={WindowDragRegion} items="center" className="pl-1.5 pr-1">*/}
|
||||
{/*</HStack>*/}
|
||||
{response?.error && (
|
||||
<div className="text-white bg-red-500 px-2 py-1 rounded">{response.error}</div>
|
||||
)}
|
||||
{response && (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<HStack
|
||||
data-tauri-drag-region
|
||||
items="center"
|
||||
className="italic text-gray-500 text-sm w-full h-10 mb-3 flex-shrink-0"
|
||||
className="italic text-gray-500 text-sm w-full mb-1 flex-shrink-0"
|
||||
>
|
||||
<div className="whitespace-nowrap">
|
||||
{response.updatedAt.toISOString()}
|
||||
•
|
||||
<div data-tauri-drag-region className="whitespace-nowrap">
|
||||
{response.status}
|
||||
{response.statusReason && ` ${response.statusReason}`}
|
||||
•
|
||||
{response.elapsed}ms •
|
||||
{Math.round(response.body.length / 1000)} KB
|
||||
</div>
|
||||
|
||||
<HStack items="center" className="ml-auto">
|
||||
{contentType.includes('html') && (
|
||||
<IconButton
|
||||
@@ -96,26 +76,49 @@ export function ResponsePane({ requestId, error }: Props) {
|
||||
onClick={() => setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))}
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: 'Clear Response',
|
||||
onSelect: deleteResponse.mutate,
|
||||
disabled: responses.data.length === 0,
|
||||
},
|
||||
{
|
||||
label: 'Clear All Responses',
|
||||
onSelect: deleteAllResponses.mutate,
|
||||
disabled: responses.data.length === 0,
|
||||
},
|
||||
'-----',
|
||||
...responses.data.slice(0, 10).map((r) => ({
|
||||
label: r.status + ' - ' + r.elapsed + ' ms',
|
||||
leftSlot: response?.id === r.id ? <Icon icon="check" /> : <></>,
|
||||
onSelect: () => setActiveResponseId(r.id),
|
||||
})),
|
||||
]}
|
||||
>
|
||||
<IconButton icon="gear" className="ml-auto" size="sm" />
|
||||
</Dropdown>
|
||||
</HStack>
|
||||
</HStack>
|
||||
{viewMode === 'pretty' && contentForIframe !== null ? (
|
||||
<iframe
|
||||
title="Response preview"
|
||||
srcDoc={contentForIframe}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
className="h-full w-full rounded-lg"
|
||||
/>
|
||||
) : response?.body ? (
|
||||
<Editor
|
||||
valueKey={`${contentType}:${response.body}`}
|
||||
defaultValue={response?.body}
|
||||
contentType={contentType}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</motion.div>
|
||||
</VStack>
|
||||
<Divider />
|
||||
</div>
|
||||
{viewMode === 'pretty' && contentForIframe !== null ? (
|
||||
<iframe
|
||||
title="Response preview"
|
||||
srcDoc={contentForIframe}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
className="h-full w-full rounded-lg"
|
||||
/>
|
||||
) : response?.body ? (
|
||||
<Editor
|
||||
valueKey={`${contentType}:${response.body}`}
|
||||
defaultValue={response?.body}
|
||||
contentType={contentType}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</LayoutPane>
|
||||
);
|
||||
}
|
||||
|
||||
34
src-web/components/ScrollArea.tsx
Normal file
34
src-web/components/ScrollArea.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as S from '@radix-ui/react-scroll-area';
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ScrollArea({ children, className }: Props) {
|
||||
return (
|
||||
<S.Root className={classnames(className, 'group')} type="always">
|
||||
<S.Viewport>{children}</S.Viewport>
|
||||
<ScrollBar orientation="vertical" />
|
||||
<ScrollBar orientation="horizontal" />
|
||||
<S.Corner />
|
||||
</S.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({ orientation }: { orientation: 'vertical' | 'horizontal' }) {
|
||||
return (
|
||||
<S.Scrollbar
|
||||
orientation={orientation}
|
||||
className={classnames(
|
||||
'flex bg-transparent rounded-full',
|
||||
orientation === 'vertical' && 'w-1',
|
||||
orientation === 'horizontal' && 'h-1 flex-col',
|
||||
)}
|
||||
>
|
||||
<S.Thumb className="flex-1 bg-gray-50 group-hover:bg-gray-100 rounded-full" />
|
||||
</S.Scrollbar>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import useTheme from '../hooks/useTheme';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { Button } from './Button';
|
||||
import { Dialog } from './Dialog';
|
||||
import { DropdownMenuRadio } from './Dropdown';
|
||||
import { HeaderEditor } from './HeaderEditor';
|
||||
import { IconButton } from './IconButton';
|
||||
import { Input } from './Input';
|
||||
@@ -24,10 +25,7 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
|
||||
const { toggleTheme } = useTheme();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
return (
|
||||
<div
|
||||
className={classnames(className, 'w-52 bg-gray-50/40 h-full border-gray-500/10')}
|
||||
{...props}
|
||||
>
|
||||
<div className={classnames(className, 'w-52 bg-gray-50 h-full')} {...props}>
|
||||
<HStack as={WindowDragRegion} items="center" className="pr-1" justify="end">
|
||||
<Dialog wide open={open} onOpenChange={setOpen} title="Edit Headers">
|
||||
<HeaderEditor />
|
||||
@@ -51,7 +49,7 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
<VStack as="ul" className="py-3" space={1}>
|
||||
<VStack as="ul" className="pb-3" space={1}>
|
||||
{requests.map((r) => (
|
||||
<SidebarItem key={r.id} request={r} active={r.id === activeRequestId} />
|
||||
))}
|
||||
@@ -66,7 +64,7 @@ function SidebarItem({ request, active }: { request: HttpRequest; active: boolea
|
||||
<Button
|
||||
as={Link}
|
||||
to={`/workspaces/${request.workspaceId}/requests/${request.id}`}
|
||||
className={classnames('w-full', active && 'bg-gray-50')}
|
||||
className={classnames('w-full', active && 'bg-gray-500/[0.1]')}
|
||||
size="xs"
|
||||
justify="start"
|
||||
>
|
||||
|
||||
@@ -34,6 +34,7 @@ export function HStack({ className, space, children, ...props }: HStackProps) {
|
||||
{i > 0 ? (
|
||||
<div
|
||||
className={classnames(spaceClassesX[space], 'pointer-events-none')}
|
||||
data-spacer=""
|
||||
aria-hidden
|
||||
/>
|
||||
) : null}
|
||||
@@ -61,6 +62,7 @@ export function VStack({ className, space, children, ...props }: VStackProps) {
|
||||
{i > 0 ? (
|
||||
<div
|
||||
className={classnames(spaceClassesY[space], 'pointer-events-none')}
|
||||
data-spacer=""
|
||||
aria-hidden
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -11,9 +11,18 @@ interface Props {
|
||||
url: string;
|
||||
onMethodChange: (method: string) => void;
|
||||
onUrlChange: (url: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChange, url }: Props) {
|
||||
export function UrlBar({
|
||||
className,
|
||||
sendRequest,
|
||||
loading,
|
||||
onMethodChange,
|
||||
method,
|
||||
onUrlChange,
|
||||
url,
|
||||
}: Props) {
|
||||
const handleSendRequest = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
sendRequest();
|
||||
@@ -23,13 +32,12 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
|
||||
<form onSubmit={handleSendRequest} className="w-full flex items-center">
|
||||
<Input
|
||||
hideLabel
|
||||
useEditor
|
||||
useTemplating
|
||||
onSubmit={sendRequest}
|
||||
contentType="url"
|
||||
useEditor={{ useTemplating: true, contentType: 'url' }}
|
||||
size="sm"
|
||||
className="font-mono text-sm"
|
||||
name="url"
|
||||
label="Enter URL"
|
||||
className="font-mono"
|
||||
containerClassName={className}
|
||||
onChange={onUrlChange}
|
||||
defaultValue={url}
|
||||
placeholder="Enter a URL..."
|
||||
@@ -51,7 +59,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
|
||||
type="button"
|
||||
disabled={loading}
|
||||
size="sm"
|
||||
className="ml-1 !px-2"
|
||||
className="ml-1 mr-2 !px-2 !text-gray-800"
|
||||
justify="start"
|
||||
>
|
||||
{method.toUpperCase()}
|
||||
@@ -60,11 +68,13 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
|
||||
}
|
||||
rightSlot={
|
||||
<IconButton
|
||||
type="submit"
|
||||
size="sm"
|
||||
icon={loading ? 'update' : 'paper-plane'}
|
||||
spin={loading}
|
||||
disabled={loading}
|
||||
size="sm"
|
||||
className="mr-1 !px-2"
|
||||
className="mx-1 !px-4"
|
||||
title="Send Request"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import classnames from 'classnames';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
type Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export function WindowDragRegion({ className, ...props }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={classnames(className, 'w-full h-11 flex-shrink-0 border-b border-gray-500/10')}
|
||||
className={classnames(className, 'w-full h-14 flex-shrink-0')}
|
||||
data-tauri-drag-region=""
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -29,6 +29,18 @@ html, body, #root {
|
||||
transition: background-color var(--transition-duration), border-color var(--transition-duration);
|
||||
}
|
||||
|
||||
/*.hide-scrollbar {*/
|
||||
/* &::-webkit-scrollbar-corner,*/
|
||||
/* &::-webkit-scrollbar {*/
|
||||
/* @apply w-[5px] h-[5px];*/
|
||||
/* background-color: transparent; !* or add it to the track *!*/
|
||||
/* }*/
|
||||
|
||||
/* &::-webkit-scrollbar-thumb {*/
|
||||
/* @apply bg-gray-100 bg-opacity-20 rounded-full;*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
|
||||
@layer base {
|
||||
:root, [data-theme="light"] {
|
||||
/* Colors */
|
||||
|
||||
@@ -98,7 +98,7 @@ const router = createBrowserRouter([
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MotionConfig transition={{ duration: 0.15 }}>
|
||||
<MotionConfig transition={{ duration: 0.1 }}>
|
||||
<HelmetProvider>
|
||||
<RouterProvider router={router} />
|
||||
</HelmetProvider>
|
||||
|
||||
Reference in New Issue
Block a user