mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:13:51 +01:00
Fix Codemirror performance!!
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
.cm-wrapper {
|
||||
@apply h-full;
|
||||
@apply h-full overflow-hidden;
|
||||
|
||||
.cm-editor {
|
||||
@apply w-full block text-base;
|
||||
@@ -109,8 +109,7 @@
|
||||
@apply hover:text-gray-800 hover:border-gray-400;
|
||||
}
|
||||
|
||||
.cm-editor .cm-activeLineGutter,
|
||||
.cm-editor .cm-activeLine {
|
||||
.cm-editor .cm-activeLineGutter {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
@@ -118,6 +117,7 @@
|
||||
&.cm-focused .cm-activeLineGutter {
|
||||
@apply text-gray-800;
|
||||
}
|
||||
|
||||
.cm-cursor {
|
||||
@apply border-l-2 border-gray-800;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { useUnmount } from 'react-use';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
||||
import classnames from 'classnames';
|
||||
import { EditorView } from 'codemirror';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import './Editor.css';
|
||||
import { singleLineExt } from './singleLine';
|
||||
|
||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||
// const { baseExtensions, getLanguageExtension, multiLineExtensions } = await import('./extensions');
|
||||
import { singleLineExt } from './singleLine';
|
||||
|
||||
export interface EditorProps {
|
||||
id?: string;
|
||||
@@ -17,7 +16,6 @@ export interface EditorProps {
|
||||
heightMode?: 'auto' | 'full';
|
||||
contentType?: string;
|
||||
autoFocus?: boolean;
|
||||
valueKey?: string | number;
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
tooltipContainer?: HTMLElement;
|
||||
@@ -32,75 +30,65 @@ export function Editor({
|
||||
contentType,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
valueKey,
|
||||
useTemplating,
|
||||
defaultValue,
|
||||
onChange,
|
||||
className,
|
||||
singleLine,
|
||||
...props
|
||||
}: EditorProps) {
|
||||
const [cm, setCm] = useState<{ view: EditorView; langHolder: Compartment } | null>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const extensions = useMemo(
|
||||
() =>
|
||||
getExtensions({
|
||||
container: ref.current,
|
||||
readOnly,
|
||||
placeholder,
|
||||
singleLine,
|
||||
onChange,
|
||||
contentType,
|
||||
useTemplating,
|
||||
}),
|
||||
[contentType, ref.current],
|
||||
);
|
||||
const [divRef, setDivRef] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
// Create codemirror instance when ref initializes
|
||||
useEffect(() => {
|
||||
const parent = ref.current;
|
||||
if (parent === null) return;
|
||||
// Unmount editor when component unmounts
|
||||
useUnmount(() => cm?.view.destroy());
|
||||
|
||||
const initDivRef = (el: HTMLDivElement | null) => {
|
||||
setDivRef(el);
|
||||
if (divRef !== null || el === null) return;
|
||||
|
||||
// console.log('INIT EDITOR');
|
||||
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)],
|
||||
extensions: [
|
||||
langHolder.of(langExt),
|
||||
...getExtensions({
|
||||
container: divRef,
|
||||
readOnly,
|
||||
placeholder,
|
||||
singleLine,
|
||||
onChange,
|
||||
contentType,
|
||||
useTemplating,
|
||||
}),
|
||||
],
|
||||
});
|
||||
view = new EditorView({ state, parent });
|
||||
syncGutterBg({ parent, className });
|
||||
setCm({ view, langHolder });
|
||||
if (autoFocus && view) view.focus();
|
||||
let newView;
|
||||
if (cm) {
|
||||
newView = cm.view;
|
||||
newView.setState(state);
|
||||
} else {
|
||||
newView = new EditorView({ state, parent: el });
|
||||
}
|
||||
setCm({ view: newView, langHolder });
|
||||
syncGutterBg({ parent: el, className });
|
||||
if (autoFocus && newView) newView.focus();
|
||||
} catch (e) {
|
||||
console.log('Failed to initialize Codemirror', e);
|
||||
}
|
||||
return () => view?.destroy();
|
||||
}, [ref.current, valueKey]);
|
||||
|
||||
// Update value when valueKey changes
|
||||
// TODO: This would be more efficient but the onChange handler gets fired on update
|
||||
// useEffect(() => {
|
||||
// if (cm === null) return;
|
||||
// console.log('NEW DOC', valueKey, defaultValue);
|
||||
// cm.view.dispatch({
|
||||
// changes: { from: 0, to: cm.view.state.doc.length, insert: `${defaultValue ?? ''}` },
|
||||
// });
|
||||
// }, [valueKey]);
|
||||
};
|
||||
|
||||
// Update language extension when contentType changes
|
||||
useEffect(() => {
|
||||
if (cm === null) return;
|
||||
// console.log('UPDATE LANG');
|
||||
const ext = getLanguageExtension({ contentType, useTemplating });
|
||||
cm.view.dispatch({ effects: cm.langHolder.reconfigure(ext) });
|
||||
}, [contentType]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
ref={initDivRef}
|
||||
className={classnames(
|
||||
className,
|
||||
'cm-wrapper text-base bg-gray-50',
|
||||
@@ -108,7 +96,6 @@ export function Editor({
|
||||
singleLine ? 'cm-singleline' : 'cm-multiline',
|
||||
readOnly && 'cm-readonly',
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -142,7 +129,6 @@ function getExtensions({
|
||||
...(ext ? [ext] : []),
|
||||
...(readOnly ? [EditorState.readOnly.of(true)] : []),
|
||||
...(placeholder ? [placeholderExt(placeholder)] : []),
|
||||
|
||||
...(singleLine
|
||||
? [
|
||||
EditorView.domEventHandlers({
|
||||
|
||||
@@ -11,12 +11,15 @@ export function debouncedAutocompletionDisplay({ millis }: { millis: number }) {
|
||||
startCompletion(view);
|
||||
}, millis);
|
||||
|
||||
return EditorView.updateListener.of(({ view, docChanged }) => {
|
||||
return EditorView.updateListener.of(({ view, docChanged, focusChanged }) => {
|
||||
// const completions = currentCompletions(view.state);
|
||||
// const status = completionStatus(view.state);
|
||||
|
||||
// If the document hasn't changed, we don't need to do anything
|
||||
if (!docChanged) return;
|
||||
if (!view.hasFocus) {
|
||||
debouncedStartCompletion.cancel();
|
||||
closeCompletion(view);
|
||||
return;
|
||||
}
|
||||
|
||||
if (view.state.doc.length === 0) {
|
||||
debouncedStartCompletion.cancel();
|
||||
@@ -24,6 +27,9 @@ export function debouncedAutocompletionDisplay({ millis }: { millis: number }) {
|
||||
return;
|
||||
}
|
||||
|
||||
debouncedStartCompletion(view);
|
||||
// If the document hasn't changed, we don't need to do anything
|
||||
if (docChanged) {
|
||||
debouncedStartCompletion(view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
crosshairCursor,
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
@@ -116,7 +115,6 @@ export const baseExtensions = [
|
||||
|
||||
export const multiLineExtensions = [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
foldGutter({
|
||||
markerDOM: (open) => {
|
||||
const el = document.createElement('div');
|
||||
@@ -130,11 +128,10 @@ export const multiLineExtensions = [
|
||||
}),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSelectionMatches({ minSelectionLength: 2 }),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
|
||||
@@ -1,113 +1,99 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { HttpHeader } from '../lib/models';
|
||||
import { IconButton } from './IconButton';
|
||||
import { Input } from './Input';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
import { VStack } from './Stacks';
|
||||
|
||||
type PairWithId = { header: Partial<HttpHeader>; id: string };
|
||||
|
||||
export function HeaderEditor() {
|
||||
const [headers, setHeaders] = useState<HttpHeader[]>([]);
|
||||
const [newHeaderName, setNewHeaderName] = useState<string>('');
|
||||
const [newHeaderValue, setNewHeaderValue] = useState<string>('');
|
||||
const handleSubmit = useCallback(
|
||||
(e?: Event) => {
|
||||
e?.preventDefault();
|
||||
setHeaders([...headers, { name: newHeaderName, value: newHeaderValue }]);
|
||||
setNewHeaderName('');
|
||||
setNewHeaderValue('');
|
||||
},
|
||||
[newHeaderName, newHeaderValue],
|
||||
);
|
||||
const newPair = () => {
|
||||
return { header: { name: '', value: '' }, id: Math.random().toString() };
|
||||
};
|
||||
|
||||
const handleChangeHeader = useCallback(
|
||||
(header: Partial<HttpHeader>, index: number) => {
|
||||
setHeaders((headers) =>
|
||||
headers.map((h, i) => {
|
||||
if (i === index) return h;
|
||||
const newHeader: HttpHeader = { ...h, ...header };
|
||||
return newHeader;
|
||||
}),
|
||||
);
|
||||
},
|
||||
[headers],
|
||||
);
|
||||
const [pairs, setPairs] = useState<PairWithId[]>([newPair()]);
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
setHeaders((headers) => headers.filter((_, i) => i !== index));
|
||||
const handleChangeHeader = (pair: PairWithId) => {
|
||||
setPairs((pairs) =>
|
||||
pairs.map((p) =>
|
||||
pair.id !== p.id ? p : { id: p.id, header: { ...p.header, ...pair.header } },
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const lastPair = pairs[pairs.length - 1];
|
||||
if (lastPair === undefined) {
|
||||
setPairs([newPair()]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastPair.header.name !== '' || lastPair.header.value !== '') {
|
||||
setPairs((pairs) => [...pairs, newPair()]);
|
||||
}
|
||||
}, [pairs]);
|
||||
|
||||
const handleDelete = (pair: PairWithId) => {
|
||||
setPairs((headers) => headers.filter((p) => p.id !== pair.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="min-h-[30vh]">
|
||||
<VStack space={2}>
|
||||
{headers.map((header, i) => (
|
||||
<FormRow
|
||||
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)}
|
||||
/>
|
||||
))}
|
||||
<VStack space={2}>
|
||||
{pairs.map((p, i) => (
|
||||
<FormRow
|
||||
autoFocus
|
||||
addSubmit
|
||||
valueKey={headers.length}
|
||||
onChangeName={setNewHeaderName}
|
||||
onChangeValue={setNewHeaderValue}
|
||||
name={newHeaderName}
|
||||
value={newHeaderValue}
|
||||
key={p.id}
|
||||
pair={p}
|
||||
onChange={handleChangeHeader}
|
||||
onDelete={i < pairs.length - 1 ? handleDelete : undefined}
|
||||
/>
|
||||
</VStack>
|
||||
</form>
|
||||
))}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
function FormRow({
|
||||
autoFocus,
|
||||
valueKey,
|
||||
name,
|
||||
value,
|
||||
addSubmit,
|
||||
onChangeName,
|
||||
onChangeValue,
|
||||
pair,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: {
|
||||
autoFocus?: boolean;
|
||||
valueKey: string | number;
|
||||
name: string;
|
||||
value: string;
|
||||
addSubmit?: boolean;
|
||||
onSubmit?: () => void;
|
||||
onChangeName: (name: string) => void;
|
||||
onChangeValue: (value: string) => void;
|
||||
onDelete?: () => void;
|
||||
pair: PairWithId;
|
||||
onChange: (pair: PairWithId) => void;
|
||||
onDelete?: (pair: PairWithId) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<HStack space={2}>
|
||||
<Input
|
||||
hideLabel
|
||||
autoFocus={autoFocus}
|
||||
useEditor={{ useTemplating: true, valueKey }}
|
||||
name="name"
|
||||
label="Name"
|
||||
placeholder="name"
|
||||
defaultValue={name}
|
||||
onChange={onChangeName}
|
||||
/>
|
||||
<Input
|
||||
hideLabel
|
||||
name="value"
|
||||
label="Value"
|
||||
useEditor={{ useTemplating: true, valueKey }}
|
||||
placeholder="value"
|
||||
defaultValue={value}
|
||||
onChange={onChangeValue}
|
||||
/>
|
||||
{onDelete && <IconButton icon="trash" onClick={onDelete} />}
|
||||
</HStack>
|
||||
{addSubmit && <input type="submit" value="Add" className="sr-only" />}
|
||||
<div className="grid grid-cols-[1fr_1fr_2.5rem] grid-rows-1 gap-2 items-center">
|
||||
<Input
|
||||
hideLabel
|
||||
autoFocus={autoFocus}
|
||||
useEditor={{ useTemplating: true }}
|
||||
name="name"
|
||||
label="Name"
|
||||
placeholder="name"
|
||||
defaultValue={pair.header.name}
|
||||
onChange={(name) =>
|
||||
onChange({
|
||||
id: pair.id,
|
||||
header: { name },
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
hideLabel
|
||||
name="value"
|
||||
label="Value"
|
||||
useEditor={{ useTemplating: true }}
|
||||
placeholder="value"
|
||||
defaultValue={pair.header.value}
|
||||
onChange={(value) =>
|
||||
onChange({
|
||||
id: pair.id,
|
||||
header: { value },
|
||||
})
|
||||
}
|
||||
/>
|
||||
{onDelete && <IconButton icon="trash" onClick={() => onDelete(pair)} className="w-auto" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,22 @@ import { Icon } from './Icon';
|
||||
type Props = IconProps & ButtonProps & { iconClassName?: string; iconSize?: IconProps['size'] };
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
|
||||
{ icon, spin, className, iconClassName, size, iconSize, ...props }: Props,
|
||||
{ icon, spin, className, iconClassName, size = 'md', iconSize, ...props }: Props,
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<Button ref={ref} className={classnames(className, 'group')} size={size} {...props}>
|
||||
<Button
|
||||
ref={ref}
|
||||
className={classnames(
|
||||
className,
|
||||
'group',
|
||||
'!px-0',
|
||||
size === 'md' && 'w-9',
|
||||
size === 'sm' && 'w-9',
|
||||
)}
|
||||
size={size}
|
||||
{...props}
|
||||
>
|
||||
<Icon
|
||||
size={iconSize}
|
||||
icon={icon}
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
labelClassName?: string;
|
||||
containerClassName?: string;
|
||||
onChange?: (value: string) => void;
|
||||
useEditor?: Pick<EditorProps, 'contentType' | 'useTemplating' | 'valueKey'>;
|
||||
useEditor?: Pick<EditorProps, 'contentType' | 'useTemplating'>;
|
||||
defaultValue?: string;
|
||||
leftSlot?: ComponentChildren;
|
||||
rightSlot?: ComponentChildren;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import classnames from 'classnames';
|
||||
import { useRequestUpdate, useSendRequest } from '../hooks/useRequest';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { Button } from './Button';
|
||||
import { Editor } from './Editor/Editor';
|
||||
import { ScrollArea } from './ScrollArea';
|
||||
import { HStack } from './Stacks';
|
||||
import { HeaderEditor } from './HeaderEditor';
|
||||
import { TabContent, Tabs } from './Tabs';
|
||||
import { UrlBar } from './UrlBar';
|
||||
|
||||
interface Props {
|
||||
@@ -17,9 +16,7 @@ export function RequestPane({ fullHeight, request, className }: Props) {
|
||||
const updateRequest = useRequestUpdate(request ?? null);
|
||||
const sendRequest = useSendRequest(request ?? null);
|
||||
return (
|
||||
<div
|
||||
className={classnames(className, 'py-2 grid grid-rows-[auto_auto_minmax(0,1fr)] grid-cols-1')}
|
||||
>
|
||||
<div className={classnames(className, 'py-2 grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1')}>
|
||||
<div className="pl-2">
|
||||
<UrlBar
|
||||
key={request.id}
|
||||
@@ -33,29 +30,33 @@ export function RequestPane({ fullHeight, request, className }: Props) {
|
||||
{/*<Divider />*/}
|
||||
</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'].map((label, i) => (
|
||||
<Button
|
||||
key={label}
|
||||
size="sm"
|
||||
color={i === 0 ? 'gray' : undefined}
|
||||
className={i !== 0 ? 'opacity-80 hover:opacity-100' : undefined}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</HStack>
|
||||
</ScrollArea>
|
||||
<Editor
|
||||
className="mt-1 !bg-gray-50"
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
valueKey={request.id}
|
||||
useTemplating
|
||||
defaultValue={request.body ?? ''}
|
||||
contentType="application/json"
|
||||
onChange={(body) => updateRequest.mutate({ body })}
|
||||
/>
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ value: 'body', label: 'JSON' },
|
||||
{ value: 'params', label: 'Params' },
|
||||
{ value: 'headers', label: 'Headers' },
|
||||
{ value: 'auth', label: 'Auth' },
|
||||
]}
|
||||
className="mt-2"
|
||||
tabListClassName="px-2"
|
||||
defaultValue="body"
|
||||
label="Request body"
|
||||
>
|
||||
<TabContent value="body">
|
||||
<Editor
|
||||
key={request.id}
|
||||
className="!bg-gray-50"
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
useTemplating
|
||||
defaultValue={request.body ?? ''}
|
||||
contentType="application/json"
|
||||
onChange={(body) => updateRequest.mutate({ body })}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value="headers" className="pl-2">
|
||||
<HeaderEditor />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,8 +116,8 @@ export function ResponsePane({ requestId, className }: Props) {
|
||||
) : response?.body ? (
|
||||
<Editor
|
||||
readOnly
|
||||
key={`${contentType}:${response.updatedAt}`}
|
||||
className="bg-gray-50 dark:!bg-gray-100"
|
||||
valueKey={`${contentType}:${response.updatedAt}`}
|
||||
defaultValue={response?.body}
|
||||
contentType={contentType}
|
||||
/>
|
||||
|
||||
@@ -28,7 +28,7 @@ function ScrollBar({ orientation }: { orientation: 'vertical' | 'horizontal' })
|
||||
orientation === 'horizontal' && 'h-1.5 flex-col',
|
||||
)}
|
||||
>
|
||||
<S.Thumb className="flex-1 bg-gray-50 group-hover:bg-gray-100 rounded-full" />
|
||||
<S.Thumb className="flex-1 bg-gray-100 group-hover:bg-gray-200 rounded-full" />
|
||||
</S.Scrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import React, { useState } from 'react';
|
||||
import { useRequestCreate } from '../hooks/useRequest';
|
||||
import useTheme from '../hooks/useTheme';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { Button } from './Button';
|
||||
import { ButtonLink } from './ButtonLink';
|
||||
import { Dialog } from './Dialog';
|
||||
import { HeaderEditor } from './HeaderEditor';
|
||||
import { IconButton } from './IconButton';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
import { WindowDragRegion } from './WindowDragRegion';
|
||||
@@ -21,7 +18,6 @@ interface Props {
|
||||
export function Sidebar({ className, activeRequestId, workspaceId, requests }: Props) {
|
||||
const createRequest = useRequestCreate({ workspaceId, navigateAfter: true });
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
@@ -30,12 +26,6 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests }: P
|
||||
)}
|
||||
>
|
||||
<HStack as={WindowDragRegion} alignItems="center" justifyContent="end">
|
||||
<Dialog wide open={open} onOpenChange={setOpen} title="Edit Headers">
|
||||
<HeaderEditor />
|
||||
<Button className="ml-auto mt-5" color="primary" onClick={() => setOpen(false)}>
|
||||
Save
|
||||
</Button>
|
||||
</Dialog>
|
||||
<IconButton
|
||||
className="mx-1"
|
||||
icon="plusCircle"
|
||||
@@ -56,7 +46,6 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests }: P
|
||||
justifyContent="end"
|
||||
>
|
||||
<IconButton icon={appearance === 'dark' ? 'moon' : 'sun'} onClick={toggleAppearance} />
|
||||
<IconButton icon="rows" onClick={() => setOpen(true)} />
|
||||
</HStack>
|
||||
</VStack>
|
||||
</div>
|
||||
|
||||
74
src-web/components/Tabs.tsx
Normal file
74
src-web/components/Tabs.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as T from '@radix-ui/react-tabs';
|
||||
import classnames from 'classnames';
|
||||
import type { ComponentChildren } from 'preact';
|
||||
import { useState } from 'react';
|
||||
import { Button } from './Button';
|
||||
import { ScrollArea } from './ScrollArea';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
defaultValue?: string;
|
||||
label: string;
|
||||
tabs: { value: string; label: ComponentChildren }[];
|
||||
tabListClassName?: string;
|
||||
className?: string;
|
||||
children: ComponentChildren;
|
||||
}
|
||||
|
||||
export function Tabs({ defaultValue, label, children, tabs, className, tabListClassName }: Props) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
return (
|
||||
<T.Root
|
||||
defaultValue={defaultValue}
|
||||
onValueChange={setValue}
|
||||
className={classnames(className, 'h-full overflow-hidden')}
|
||||
>
|
||||
<T.List aria-label={label} className={classnames(tabListClassName, 'flex items-center')}>
|
||||
<ScrollArea className="w-full pb-2">
|
||||
<HStack space={1}>
|
||||
{tabs.map((t) => (
|
||||
<TabTrigger key={t.value} value={t.value} active={t.value === value}>
|
||||
{t.label}
|
||||
</TabTrigger>
|
||||
))}
|
||||
</HStack>
|
||||
</ScrollArea>
|
||||
</T.List>
|
||||
{children}
|
||||
</T.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface TabTriggerProps {
|
||||
value: string;
|
||||
children: ComponentChildren;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export function TabTrigger({ value, children, active }: TabTriggerProps) {
|
||||
return (
|
||||
<T.Trigger value={value} asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={active}
|
||||
className={classnames(active ? 'bg-gray-100' : '!text-gray-500 hover:!text-gray-800')}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
</T.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
interface TabContentProps {
|
||||
value: string;
|
||||
children: ComponentChildren;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TabContent({ value, children, className }: TabContentProps) {
|
||||
return (
|
||||
<T.Content value={value} className={classnames(className, 'w-full h-full')}>
|
||||
{children}
|
||||
</T.Content>
|
||||
);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
|
||||
<Input
|
||||
hideLabel
|
||||
useEditor={{ useTemplating: true, contentType: 'url' }}
|
||||
className="font-mono px-0"
|
||||
className="px-0"
|
||||
name="url"
|
||||
label="Enter URL"
|
||||
containerClassName="shadow shadow-gray-100 dark:shadow-gray-0"
|
||||
|
||||
Reference in New Issue
Block a user