mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-16 05:56:47 +01:00
Start of themes
This commit is contained in:
1064
package-lock.json
generated
1064
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,9 @@
|
||||
"preview": "vite preview",
|
||||
"tauri-dev": "concurrently -n app,rsw \"tauri dev\" \"rsw watch\"",
|
||||
"tauri-build": "npm run build:icon && tauri build",
|
||||
"build:icon": "tauri icon src-tauri/icons/icon.png"
|
||||
"build:icon": "tauri icon src-tauri/icons/icon.png",
|
||||
"test": "vitest",
|
||||
"coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.2.1",
|
||||
@@ -34,6 +36,7 @@
|
||||
"classnames": "^2.3.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"framer-motion": "^9.0.4",
|
||||
"parse-color": "^1.0.0",
|
||||
"parse-json": "^6.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -43,6 +46,7 @@
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.2.2",
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/parse-color": "^1.0.1",
|
||||
"@types/parse-json": "^4.0.0",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
@@ -63,6 +67,7 @@
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^4.0.0",
|
||||
"vite-plugin-rsw": "^2.0.11",
|
||||
"vite-plugin-top-level-await": "^1.2.4"
|
||||
"vite-plugin-top-level-await": "^1.2.4",
|
||||
"vitest": "^0.29.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,21 +33,21 @@ function App() {
|
||||
<div className="grid grid-rows-[auto_1fr] h-full overflow-hidden">
|
||||
<HStack
|
||||
as={WindowDragRegion}
|
||||
className="px-3 bg-gray-50/50 text-sm text-gray-900 border-b border-b-gray-50 pt-[1px]"
|
||||
className="px-3 bg-background text-sm text-gray-900 border-b border-b-gray-100 pt-[1px]"
|
||||
items="center"
|
||||
>
|
||||
{request.name}
|
||||
</HStack>
|
||||
<div
|
||||
className={classnames(
|
||||
'bg-gray-25 grid overflow-auto',
|
||||
'grid overflow-auto',
|
||||
isH ? 'grid-cols-[1fr_1fr]' : 'grid-rows-[minmax(0,auto)_minmax(0,100%)]',
|
||||
)}
|
||||
>
|
||||
<RequestPane
|
||||
fullHeight={isH}
|
||||
request={request}
|
||||
className={classnames(isH ? 'pr-0' : 'pb-3 mb-1')}
|
||||
className={classnames(!isH && 'pr-2 pb-3 mb-1')}
|
||||
/>
|
||||
<ResponsePane requestId={request.id} />
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,8 @@ import { Icon } from './Icon';
|
||||
|
||||
const colorStyles = {
|
||||
default: 'hover:bg-gray-500/10 text-gray-600',
|
||||
gray: 'text-gray-800 bg-gray-50 hover:bg-gray-500/20',
|
||||
gray: 'text-gray-800 bg-gray-100 hover:bg-gray-500/20',
|
||||
tint: 'text-white/90 hover:text-white hover:bg-white/20',
|
||||
primary: 'bg-blue-400',
|
||||
secondary: 'bg-violet-400',
|
||||
warning: 'bg-orange-400',
|
||||
@@ -45,7 +46,9 @@ export const Button = forwardRef(function Button<T extends ElementType>(
|
||||
type="button"
|
||||
className={classnames(
|
||||
className,
|
||||
'transition-all rounded-md flex items-center bg-opacity-80 hover:bg-opacity-100 hover:text-white',
|
||||
'outline-none', // TODO: Add focus styles
|
||||
'border border-transparent focus-visible:border-blue-300',
|
||||
'transition-all rounded-md flex items-center hover:text-white',
|
||||
// 'active:translate-y-[0.5px] active:scale-[0.99]',
|
||||
colorStyles[color || 'default'],
|
||||
justify === 'start' && 'justify-start',
|
||||
@@ -57,7 +60,7 @@ export const Button = forwardRef(function Button<T extends ElementType>(
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{forDropdown && <Icon icon="triangle-down" className="ml-1 -mr-1" />}
|
||||
{forDropdown && <Icon icon="triangleDown" className="ml-1 -mr-1" />}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -264,11 +264,7 @@ function DropdownMenuSeparator({ className, ...props }: D.DropdownMenuSeparatorP
|
||||
|
||||
function DropdownMenuTrigger({ children, className, ...props }: D.DropdownMenuTriggerProps) {
|
||||
return (
|
||||
<D.Trigger
|
||||
asChild
|
||||
className={classnames(className, 'focus:outline-none focus:border-0 focus:shadow-none')}
|
||||
{...props}
|
||||
>
|
||||
<D.Trigger asChild className={classnames(className)} {...props}>
|
||||
{children}
|
||||
</D.Trigger>
|
||||
);
|
||||
@@ -290,7 +286,7 @@ const ItemInner = forwardRef<HTMLDivElement, ItemInnerProps>(function ItemInner(
|
||||
ref={ref}
|
||||
className={classnames(
|
||||
className,
|
||||
'outline-none px-2 py-1.5 flex items-center text-sm text-gray-700',
|
||||
'outline-none px-2 py-1.5 flex items-center text-sm text-gray-700 whitespace-nowrap pr-4',
|
||||
!noHover && 'focus:bg-gray-50 focus:text-gray-900 rounded',
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
@apply bg-background w-full block text-[0.85rem];
|
||||
@apply w-full block text-[0.85rem];
|
||||
|
||||
&.cm-focused {
|
||||
outline: none !important;
|
||||
@@ -26,6 +26,18 @@
|
||||
@apply text-placeholder;
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
@apply border-0 text-gray-500 text-opacity-30;
|
||||
.cm-gutterElement {
|
||||
@apply cursor-default;
|
||||
}
|
||||
}
|
||||
|
||||
&.cm-focused .cm-gutters,
|
||||
.cm-gutters:hover {
|
||||
@apply text-opacity-60;
|
||||
}
|
||||
|
||||
.placeholder-widget {
|
||||
@apply text-xs text-white/90 bg-blue-400/80 py-[0.5px] 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);
|
||||
@@ -69,10 +81,6 @@
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.cm-editor .cm-gutters {
|
||||
@apply bg-background border-0 text-gray-200;
|
||||
}
|
||||
|
||||
.cm-editor .cm-gutterElement {
|
||||
transition: color var(--transition-duration);
|
||||
}
|
||||
@@ -98,10 +106,6 @@
|
||||
@apply text-gray-400 bg-gray-100/20;
|
||||
}
|
||||
|
||||
.cm-editor.cm-focused .cm-gutters {
|
||||
@apply text-gray-300;
|
||||
}
|
||||
|
||||
.cm-editor .cm-foldPlaceholder {
|
||||
@apply px-2 border border-gray-200 bg-gray-100;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
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';
|
||||
import { EditorView } from 'codemirror';
|
||||
import type { CSSProperties, HTMLAttributes } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import './Editor.css';
|
||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||
import { singleLineExt } from './singleLine';
|
||||
|
||||
export interface EditorProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||
height?: 'auto' | 'full';
|
||||
heightMode?: 'auto' | 'full';
|
||||
contentType?: string;
|
||||
backgroundColor?: string;
|
||||
autoFocus?: boolean;
|
||||
valueKey?: string | number;
|
||||
defaultValue?: string;
|
||||
@@ -25,9 +23,8 @@ export interface EditorProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onCha
|
||||
}
|
||||
|
||||
export default function Editor({
|
||||
height,
|
||||
heightMode,
|
||||
contentType,
|
||||
backgroundColor,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
valueKey,
|
||||
@@ -53,6 +50,18 @@ export default function Editor({
|
||||
[contentType, ref.current],
|
||||
);
|
||||
|
||||
const syncGutterBg = () => {
|
||||
if (ref.current === null) return;
|
||||
if (singleLine) return;
|
||||
const gutterEl = ref.current.querySelector<HTMLDivElement>('.cm-gutters');
|
||||
const bgClass = className
|
||||
?.split(/\s+/)
|
||||
.find((c) => c.startsWith('!bg-') || c.startsWith('bg-'));
|
||||
if (bgClass && gutterEl) {
|
||||
gutterEl?.classList.add(`${bgClass}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Create codemirror instance when ref initializes
|
||||
useEffect(() => {
|
||||
if (ref.current === null) return;
|
||||
@@ -69,6 +78,7 @@ export default function Editor({
|
||||
parent: ref.current,
|
||||
});
|
||||
setCm({ view, langHolder });
|
||||
syncGutterBg();
|
||||
if (autoFocus && view) view.focus();
|
||||
} catch (e) {
|
||||
console.log('Failed to initialize Codemirror', e);
|
||||
@@ -76,6 +86,10 @@ export default function Editor({
|
||||
return () => view?.destroy();
|
||||
}, [ref.current, valueKey]);
|
||||
|
||||
useEffect(() => {
|
||||
syncGutterBg();
|
||||
}, [ref.current, className]);
|
||||
|
||||
// Update value when valueKey changes
|
||||
// TODO: This would be more efficient but the onChange handler gets fired on update
|
||||
// useEffect(() => {
|
||||
@@ -98,11 +112,10 @@ export default function Editor({
|
||||
ref={ref}
|
||||
className={classnames(
|
||||
className,
|
||||
'cm-wrapper text-base',
|
||||
height === 'auto' ? 'cm-auto-height' : 'cm-full-height',
|
||||
'cm-wrapper text-base bg-background',
|
||||
heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height',
|
||||
singleLine ? 'cm-singleline' : 'cm-multiline',
|
||||
)}
|
||||
data-color-background="var(--color-gray-50)"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -158,24 +171,3 @@ 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)],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ export const myHighlightStyle = HighlightStyle.define([
|
||||
{ tag: [t.attributeName], color: 'hsl(var(--color-violet-600))' },
|
||||
{ tag: [t.attributeValue], color: 'hsl(var(--color-orange-600))' },
|
||||
{ tag: [t.string], color: 'hsl(var(--color-yellow-600))' },
|
||||
{ tag: [t.keyword, t.meta, t.operator], color: '#45e8a4' },
|
||||
{ tag: [t.keyword, t.meta, t.operator], color: 'hsl(var(--color-red-600))' },
|
||||
]);
|
||||
|
||||
// export const defaultHighlightStyle = HighlightStyle.define([
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ArchiveIcon,
|
||||
CameraIcon,
|
||||
CheckIcon,
|
||||
ClockIcon,
|
||||
CodeIcon,
|
||||
Cross2Icon,
|
||||
EyeOpenIcon,
|
||||
@@ -15,6 +16,8 @@ import {
|
||||
SunIcon,
|
||||
TrashIcon,
|
||||
TriangleDownIcon,
|
||||
TriangleLeftIcon,
|
||||
TriangleRightIcon,
|
||||
UpdateIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import classnames from 'classnames';
|
||||
@@ -26,13 +29,16 @@ type IconName =
|
||||
| 'camera'
|
||||
| 'gear'
|
||||
| 'eye'
|
||||
| 'triangle-down'
|
||||
| 'paper-plane'
|
||||
| 'triangleDown'
|
||||
| 'triangleLeft'
|
||||
| 'triangleRight'
|
||||
| 'paperPlane'
|
||||
| 'update'
|
||||
| 'question'
|
||||
| 'check'
|
||||
| 'plus'
|
||||
| 'plus-circled'
|
||||
| 'plusCircle'
|
||||
| 'clock'
|
||||
| 'sun'
|
||||
| 'code'
|
||||
| 'x'
|
||||
@@ -40,13 +46,16 @@ type IconName =
|
||||
| 'moon';
|
||||
|
||||
const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = {
|
||||
'paper-plane': PaperPlaneIcon,
|
||||
'triangle-down': TriangleDownIcon,
|
||||
paperPlane: PaperPlaneIcon,
|
||||
triangleDown: TriangleDownIcon,
|
||||
plus: PlusIcon,
|
||||
'plus-circled': PlusCircledIcon,
|
||||
plusCircle: PlusCircledIcon,
|
||||
clock: ClockIcon,
|
||||
archive: ArchiveIcon,
|
||||
camera: CameraIcon,
|
||||
check: CheckIcon,
|
||||
triangleLeft: TriangleLeftIcon,
|
||||
triangleRight: TriangleRightIcon,
|
||||
gear: GearIcon,
|
||||
home: HomeIcon,
|
||||
update: UpdateIcon,
|
||||
|
||||
@@ -5,18 +5,22 @@ import type { ButtonProps } from './Button';
|
||||
import { Button } from './Button';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type Props = Omit<IconProps, 'size'> & ButtonProps<typeof Button>;
|
||||
type Props = Omit<IconProps, 'size'> &
|
||||
ButtonProps<typeof Button> & {
|
||||
iconClassName?: string;
|
||||
};
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
|
||||
{ icon, spin, ...props }: Props,
|
||||
{ icon, spin, className, iconClassName, ...props }: Props,
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<Button ref={ref} className="group" {...props}>
|
||||
<Button ref={ref} className={classnames(className, 'group')} {...props}>
|
||||
<Icon
|
||||
icon={icon}
|
||||
spin={spin}
|
||||
className={classnames(
|
||||
iconClassName,
|
||||
'text-gray-700 group-hover:text-gray-900',
|
||||
props.disabled && 'opacity-70',
|
||||
)}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function Input({
|
||||
className={classnames(
|
||||
containerClassName,
|
||||
'relative w-full rounded-md text-gray-900 bg-gray-200/10',
|
||||
'border border-gray-500/10 focus-within:border-blue-400/40',
|
||||
'border border-gray-50 focus-within:border-blue-400/40',
|
||||
size === 'md' && 'h-10',
|
||||
size === 'sm' && 'h-8',
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function RequestPane({ fullHeight, request, className }: Props) {
|
||||
>
|
||||
<div className="pl-2">
|
||||
<UrlBar
|
||||
className="border-0 mb-1"
|
||||
className="bg-transparent"
|
||||
key={request.id}
|
||||
method={request.method}
|
||||
url={request.url}
|
||||
@@ -36,28 +36,27 @@ export function RequestPane({ fullHeight, request, className }: Props) {
|
||||
{/*<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) => (
|
||||
{['JSON', 'Params', 'Headers', 'Auth'].map((label, i) => (
|
||||
<Button
|
||||
key={label}
|
||||
size="xs"
|
||||
color={i === 0 && 'gray'}
|
||||
className={i !== 0 && 'opacity-50 hover:opacity-60'}
|
||||
className={i !== 0 && 'opacity-80 hover:opacity-100'}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</HStack>
|
||||
</ScrollArea>
|
||||
<div className="px-0">
|
||||
<Editor
|
||||
height={fullHeight ? 'full' : 'auto'}
|
||||
valueKey={request.id}
|
||||
useTemplating
|
||||
defaultValue={request.body ?? ''}
|
||||
contentType="application/json"
|
||||
onChange={(body) => updateRequest.mutate({ body })}
|
||||
/>
|
||||
</div>
|
||||
<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 })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function ResponsePane({ requestId, className }: Props) {
|
||||
<div
|
||||
className={classnames(
|
||||
className,
|
||||
'max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 bg-gray-50/50 rounded-md',
|
||||
'max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 bg-gray-100 rounded-md overflow-hidden border border-gray-50',
|
||||
)}
|
||||
>
|
||||
{/*<HStack as={WindowDragRegion} items="center" className="pl-1.5 pr-1">*/}
|
||||
@@ -58,7 +58,7 @@ export function ResponsePane({ requestId, className }: Props) {
|
||||
<>
|
||||
<HStack
|
||||
items="center"
|
||||
className="italic text-gray-500 text-sm w-full mb-1 flex-shrink-0 pl-2 py-1"
|
||||
className="italic text-gray-500 text-sm w-full mb-1 flex-shrink-0 pl-2"
|
||||
>
|
||||
<div className="whitespace-nowrap">
|
||||
{response.status}
|
||||
@@ -68,7 +68,7 @@ export function ResponsePane({ requestId, className }: Props) {
|
||||
{Math.round(response.body.length / 1000)} KB
|
||||
</div>
|
||||
|
||||
<HStack items="center" className="ml-auto">
|
||||
<HStack items="center" className="ml-auto h-8">
|
||||
{contentType.includes('html') && (
|
||||
<IconButton
|
||||
icon={viewMode === 'pretty' ? 'eye' : 'code'}
|
||||
@@ -97,7 +97,12 @@ export function ResponsePane({ requestId, className }: Props) {
|
||||
})),
|
||||
]}
|
||||
>
|
||||
<IconButton icon="gear" className="ml-auto" size="sm" />
|
||||
<IconButton
|
||||
icon="clock"
|
||||
className="ml-auto"
|
||||
iconClassName="text-gray-300"
|
||||
size="sm"
|
||||
/>
|
||||
</Dropdown>
|
||||
</HStack>
|
||||
</HStack>
|
||||
@@ -113,7 +118,7 @@ export function ResponsePane({ requestId, className }: Props) {
|
||||
</div>
|
||||
) : response?.body ? (
|
||||
<Editor
|
||||
backgroundColor="red"
|
||||
className="mr-1 !bg-gray-100"
|
||||
valueKey={`${contentType}:${response.body}`}
|
||||
defaultValue={response?.body}
|
||||
contentType={contentType}
|
||||
|
||||
@@ -20,11 +20,14 @@ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
||||
|
||||
export function Sidebar({ className, activeRequestId, workspaceId, requests, ...props }: Props) {
|
||||
const createRequest = useRequestCreate({ workspaceId, navigateAfter: true });
|
||||
const { toggleTheme } = useTheme();
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
return (
|
||||
<div
|
||||
className={classnames(className, 'w-52 bg-gray-50 h-full border-gray-100/50 relative z-10')}
|
||||
className={classnames(
|
||||
className,
|
||||
'w-52 bg-violet-600 dark:bg-violet-50 h-full border-gray-100/50 relative z-10',
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<HStack as={WindowDragRegion} items="center" justify="end">
|
||||
@@ -34,17 +37,14 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
|
||||
Save
|
||||
</Button>
|
||||
</Dialog>
|
||||
{/*<IconButton*/}
|
||||
{/* size="sm"*/}
|
||||
{/* icon="camera"*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* setOpen((v) => !v);*/}
|
||||
{/* }}*/}
|
||||
{/*/>*/}
|
||||
<IconButton size="sm" icon="sun" onClick={toggleTheme} />
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon="plus-circled"
|
||||
icon={appearance === 'dark' ? 'moon' : 'sun'}
|
||||
onClick={toggleAppearance}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon="plusCircle"
|
||||
onClick={async () => {
|
||||
await createRequest.mutate({ name: 'Test Request' });
|
||||
}}
|
||||
@@ -54,6 +54,19 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
|
||||
{requests.map((r) => (
|
||||
<SidebarItem key={r.id} request={r} active={r.id === activeRequestId} />
|
||||
))}
|
||||
<div>
|
||||
<div className="w-10 h-5 bg-blue-50" />
|
||||
<div className="w-10 h-5 bg-blue-100" />
|
||||
<div className="w-10 h-5 bg-blue-200" />
|
||||
<div className="w-10 h-5 bg-blue-300" />
|
||||
<div className="w-10 h-5 bg-blue-400" />
|
||||
<div className="w-10 h-5 bg-blue-500" />
|
||||
<div className="w-10 h-5 bg-blue-600" />
|
||||
<div className="w-10 h-5 bg-blue-700" />
|
||||
<div className="w-10 h-5 bg-blue-800" />
|
||||
<div className="w-10 h-5 bg-blue-900" />
|
||||
<div className="w-10 h-5 bg-blue-950" />
|
||||
</div>
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
@@ -64,12 +77,13 @@ function SidebarItem({ request, active }: { request: HttpRequest; active: boolea
|
||||
<li key={request.id}>
|
||||
<Button
|
||||
as={Link}
|
||||
color="tint"
|
||||
to={`/workspaces/${request.workspaceId}/requests/${request.id}`}
|
||||
className={classnames('w-full', active && 'bg-gray-500/[0.1] text-gray-900')}
|
||||
size="xs"
|
||||
justify="start"
|
||||
>
|
||||
{request.name}
|
||||
{request.name || request.url}
|
||||
</Button>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -58,8 +58,8 @@ export function UrlBar({
|
||||
<Button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
size="sm"
|
||||
className="ml-1 mr-2 !px-2 !text-gray-800"
|
||||
size="xs"
|
||||
className="mx-0.5 !text-gray-800"
|
||||
justify="start"
|
||||
>
|
||||
{method.toUpperCase()}
|
||||
@@ -69,11 +69,11 @@ export function UrlBar({
|
||||
rightSlot={
|
||||
<IconButton
|
||||
type="submit"
|
||||
size="sm"
|
||||
icon={loading ? 'update' : 'paper-plane'}
|
||||
className="mr-0.5"
|
||||
size="xs"
|
||||
icon={loading ? 'update' : 'paperPlane'}
|
||||
spin={loading}
|
||||
disabled={loading}
|
||||
className="mx-1 !px-4"
|
||||
title="Send Request"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect } from 'react';
|
||||
import { setTheme, subscribeToPreferredThemeChange, toggleTheme } from '../lib/theme';
|
||||
import type { Appearance } from '../lib/theme/window';
|
||||
import {
|
||||
getAppearance,
|
||||
setAppearance,
|
||||
subscribeToPreferredAppearanceChange,
|
||||
toggleAppearance,
|
||||
} from '../lib/theme/window';
|
||||
|
||||
const appearanceQueryKey = ['theme', 'appearance'];
|
||||
|
||||
export default function useTheme() {
|
||||
const queryClient = useQueryClient();
|
||||
const appearance = useQuery({
|
||||
queryKey: appearanceQueryKey,
|
||||
queryFn: getAppearance,
|
||||
initialData: getAppearance(),
|
||||
});
|
||||
|
||||
const themeChange = (appearance: Appearance) => {
|
||||
setAppearance(appearance);
|
||||
};
|
||||
|
||||
const handleToggleAppearance = async () => {
|
||||
const newAppearance = toggleAppearance();
|
||||
await queryClient.setQueryData(appearanceQueryKey, newAppearance);
|
||||
};
|
||||
|
||||
export default function useTheme(subscribeToChanges = true): { toggleTheme: () => void } {
|
||||
useEffect(() => {
|
||||
if (!subscribeToChanges) return;
|
||||
const unsub = subscribeToPreferredThemeChange(setTheme);
|
||||
return unsub;
|
||||
}, [subscribeToChanges]);
|
||||
return subscribeToPreferredAppearanceChange(themeChange);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
toggleTheme: toggleTheme,
|
||||
appearance: appearance.data,
|
||||
toggleAppearance: handleToggleAppearance,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface HttpResponse extends BaseModel {
|
||||
requestId: string;
|
||||
body: string;
|
||||
error: string;
|
||||
status: string;
|
||||
status: number;
|
||||
elapsed: number;
|
||||
statusReason: string;
|
||||
url: string;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
export type Theme = 'dark' | 'light';
|
||||
|
||||
export function toggleTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') ?? getPreferredTheme();
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
}
|
||||
|
||||
export function setTheme(theme?: Theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme ?? getPreferredTheme());
|
||||
}
|
||||
|
||||
export function getPreferredTheme(): Theme {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
export function subscribeToPreferredThemeChange(cb: (theme: Theme) => void): () => void {
|
||||
const listener = (e: MediaQueryListEvent) => cb(e.matches ? 'dark' : 'light');
|
||||
const m = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
m.addEventListener('change', listener);
|
||||
return () => m.removeEventListener('change', listener);
|
||||
}
|
||||
39
src-web/lib/theme/theme.test.ts
Normal file
39
src-web/lib/theme/theme.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { generateColorVariant, toTailwindVariable } from './theme';
|
||||
|
||||
describe('suite name', () => {
|
||||
it('Generates dark variants', () => {
|
||||
expect(generateColorVariant('blue', 50, 'dark')).toEqual('hsl(240,100%,5.0%)');
|
||||
expect(generateColorVariant('blue', 100, 'dark')).toEqual('hsl(240,100%,10.0%)');
|
||||
expect(generateColorVariant('blue', 200, 'dark')).toEqual('hsl(240,100%,20.0%)');
|
||||
expect(generateColorVariant('blue', 300, 'dark')).toEqual('hsl(240,100%,30.0%)');
|
||||
expect(generateColorVariant('blue', 400, 'dark')).toEqual('hsl(240,100%,40.0%)');
|
||||
expect(generateColorVariant('blue', 500, 'dark')).toEqual('hsl(240,100%,50.0%)');
|
||||
expect(generateColorVariant('blue', 600, 'dark')).toEqual('hsl(240,100%,60.0%)');
|
||||
expect(generateColorVariant('blue', 700, 'dark')).toEqual('hsl(240,100%,70.0%)');
|
||||
expect(generateColorVariant('blue', 800, 'dark')).toEqual('hsl(240,100%,80.0%)');
|
||||
expect(generateColorVariant('blue', 900, 'dark')).toEqual('hsl(240,100%,90.0%)');
|
||||
expect(generateColorVariant('blue', 950, 'dark')).toEqual('hsl(240,100%,95.0%)');
|
||||
});
|
||||
it('Generates light variants', () => {
|
||||
expect(generateColorVariant('blue', 50, 'light')).toEqual('hsl(240,100%,95.0%)');
|
||||
expect(generateColorVariant('blue', 100, 'light')).toEqual('hsl(240,100%,90.0%)');
|
||||
expect(generateColorVariant('blue', 200, 'light')).toEqual('hsl(240,100%,80.0%)');
|
||||
expect(generateColorVariant('blue', 300, 'light')).toEqual('hsl(240,100%,70.0%)');
|
||||
expect(generateColorVariant('blue', 400, 'light')).toEqual('hsl(240,100%,60.0%)');
|
||||
expect(generateColorVariant('blue', 500, 'light')).toEqual('hsl(240,100%,50.0%)');
|
||||
expect(generateColorVariant('blue', 600, 'light')).toEqual('hsl(240,100%,40.0%)');
|
||||
expect(generateColorVariant('blue', 700, 'light')).toEqual('hsl(240,100%,30.0%)');
|
||||
expect(generateColorVariant('blue', 800, 'light')).toEqual('hsl(240,100%,20.0%)');
|
||||
expect(generateColorVariant('blue', 900, 'light')).toEqual('hsl(240,100%,10.0%)');
|
||||
expect(generateColorVariant('blue', 950, 'light')).toEqual('hsl(240,100%,5.0%)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Generates Tailwind color', () => {
|
||||
it('Does it', () => {
|
||||
expect(
|
||||
toTailwindVariable({ name: 'blue', cssColor: 'hsl(10, 20%, 30%)', variant: 100 }),
|
||||
).toEqual('--color-blue-100: 10 20% 30%;');
|
||||
});
|
||||
});
|
||||
115
src-web/lib/theme/theme.ts
Normal file
115
src-web/lib/theme/theme.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import parseColor from 'parse-color';
|
||||
import type { Appearance } from './window';
|
||||
|
||||
export type AppThemeColor =
|
||||
| 'gray'
|
||||
| 'red'
|
||||
| 'orange'
|
||||
| 'yellow'
|
||||
| 'green'
|
||||
| 'blue'
|
||||
| 'pink'
|
||||
| 'violet';
|
||||
const colorNames: AppThemeColor[] = [
|
||||
'gray',
|
||||
'red',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'blue',
|
||||
'pink',
|
||||
'violet',
|
||||
];
|
||||
export type AppThemeColorVariant = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
|
||||
export const appThemeVariants: AppThemeColorVariant[] = [
|
||||
50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950,
|
||||
];
|
||||
|
||||
export type AppThemeLayer = 'root' | 'sidebar' | 'titlebar' | 'content' | 'above';
|
||||
|
||||
export interface AppThemeLayerStyle {
|
||||
colors: Record<AppThemeColor, string>;
|
||||
}
|
||||
|
||||
interface ThemeColorObj {
|
||||
name: AppThemeColor;
|
||||
variant: AppThemeColorVariant;
|
||||
cssColor: string;
|
||||
}
|
||||
|
||||
export interface AppTheme {
|
||||
name: string;
|
||||
appearance: Appearance;
|
||||
layers: Partial<Record<AppThemeLayer, AppThemeLayerStyle>>;
|
||||
}
|
||||
|
||||
export function generateCSS(t: AppTheme): ThemeColorObj[] {
|
||||
const rootColors = t.layers.root?.colors;
|
||||
if (rootColors === undefined) return [];
|
||||
|
||||
const colors: ThemeColorObj[] = [];
|
||||
for (const color of colorNames) {
|
||||
const rawValue = rootColors[color];
|
||||
if (!rawValue) continue;
|
||||
colors.push(...generateColors(color, rawValue, t.appearance));
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
export function generateColors(
|
||||
name: AppThemeColor,
|
||||
color: string,
|
||||
appearance: Appearance,
|
||||
): ThemeColorObj[] {
|
||||
const colors = [];
|
||||
for (const variant of appThemeVariants) {
|
||||
colors.push({ name, variant, cssColor: generateColorVariant(color, variant, appearance) });
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
const lightnessMap: Record<Appearance, Record<AppThemeColorVariant, number>> = {
|
||||
light: {
|
||||
50: 1,
|
||||
100: 0.8,
|
||||
200: 0.7,
|
||||
300: 0.5,
|
||||
400: 0.3,
|
||||
500: 0.1,
|
||||
600: -0.2,
|
||||
700: -0.3,
|
||||
800: -0.5,
|
||||
900: -0.7,
|
||||
950: -0.8,
|
||||
},
|
||||
dark: {
|
||||
50: -0.95,
|
||||
100: -0.8,
|
||||
200: -0.6,
|
||||
300: -0.4,
|
||||
400: -0.2,
|
||||
500: 0,
|
||||
600: 0.2,
|
||||
700: 0.4,
|
||||
800: 0.5,
|
||||
900: 0.7,
|
||||
950: 0.9,
|
||||
},
|
||||
};
|
||||
|
||||
export function generateColorVariant(
|
||||
color: string,
|
||||
variant: AppThemeColorVariant,
|
||||
appearance: Appearance,
|
||||
): string {
|
||||
const { hsl } = parseColor(color || '');
|
||||
const lightnessMod = lightnessMap[appearance][variant];
|
||||
const newL = hsl[2] + (100 - hsl[2]) * lightnessMod;
|
||||
return `hsl(${hsl[0]},${hsl[1]}%,${newL.toFixed(1)}%)`;
|
||||
}
|
||||
|
||||
export function toTailwindVariable({ name, variant, cssColor }: ThemeColorObj): string {
|
||||
const { hsl } = parseColor(cssColor || '');
|
||||
return `--color-${name}-${variant}: ${hsl[0]} ${hsl[1]}% ${hsl[2]}%;`;
|
||||
}
|
||||
92
src-web/lib/theme/window.ts
Normal file
92
src-web/lib/theme/window.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { AppTheme } from './theme';
|
||||
import { generateCSS, toTailwindVariable } from './theme';
|
||||
|
||||
export type Appearance = 'dark' | 'light';
|
||||
|
||||
const darkTheme: AppTheme = {
|
||||
name: 'Default Dark',
|
||||
appearance: 'dark',
|
||||
layers: {
|
||||
root: {
|
||||
colors: {
|
||||
gray: '#69789b',
|
||||
red: '#ff1c1c',
|
||||
orange: '#ff9411',
|
||||
yellow: '#ffff1f',
|
||||
green: '#35ff35',
|
||||
blue: '#1365ff',
|
||||
pink: '#ff74ff',
|
||||
violet: '#873fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const lightTheme: AppTheme = {
|
||||
name: 'Default Light',
|
||||
appearance: 'light',
|
||||
layers: {
|
||||
root: {
|
||||
colors: {
|
||||
gray: '#69789b',
|
||||
red: '#e13939',
|
||||
orange: '#da881f',
|
||||
yellow: '#e3b22d',
|
||||
green: '#37c237',
|
||||
blue: '#1365ff',
|
||||
pink: '#e861e8',
|
||||
violet: '#8d47ff',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getAppearance(): Appearance {
|
||||
const docAppearance = document.documentElement.getAttribute('data-appearance');
|
||||
if (docAppearance === 'dark' || docAppearance === 'light') {
|
||||
return docAppearance;
|
||||
}
|
||||
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;
|
||||
document.documentElement.setAttribute('data-appearance', appearance);
|
||||
document.documentElement.setAttribute('data-theme', theme.name);
|
||||
|
||||
let existingStyleEl = document.head.querySelector(`style[data-theme-definition="${theme.name}"]`);
|
||||
if (!existingStyleEl) {
|
||||
const styleEl = document.createElement('style');
|
||||
document.head.appendChild(styleEl);
|
||||
existingStyleEl = styleEl;
|
||||
}
|
||||
|
||||
existingStyleEl.textContent = [
|
||||
`[data-theme="${theme.name}"] {`,
|
||||
...generateCSS(theme).map(toTailwindVariable),
|
||||
'}',
|
||||
].join('\n');
|
||||
existingStyleEl.setAttribute('data-theme-definition', theme.name);
|
||||
}
|
||||
|
||||
export function getPreferredAppearance(): Appearance {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
export function subscribeToPreferredAppearanceChange(
|
||||
cb: (appearance: Appearance) => void,
|
||||
): () => void {
|
||||
const listener = (e: MediaQueryListEvent) => cb(e.matches ? 'dark' : 'light');
|
||||
const m = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
m.addEventListener('change', listener);
|
||||
return () => m.removeEventListener('change', listener);
|
||||
}
|
||||
@@ -41,17 +41,15 @@ html, body, #root {
|
||||
/* }*/
|
||||
/*}*/
|
||||
|
||||
[data-color-background] {
|
||||
--color-background: attr(data-bg-color);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root, [data-theme="light"] {
|
||||
/* Colors */
|
||||
:root {
|
||||
--color-white: 255 100% 100%;
|
||||
--color-black: 255 0% 0%;
|
||||
--color-background: var(--color-white);
|
||||
--color-background: var(--color-gray-50);
|
||||
}
|
||||
|
||||
:root, [data-theme="light"] {
|
||||
/* Colors */
|
||||
--color-green-50: 160 84% 95%;
|
||||
--color-green-100: 160 84% 88%;
|
||||
--color-green-200: 160 84% 76%;
|
||||
@@ -61,7 +59,7 @@ html, body, #root {
|
||||
--color-green-600: 160 84% 38%;
|
||||
--color-green-700: 160 84% 30%;
|
||||
--color-green-800: 160 84% 20%;
|
||||
--color-green-900: 160 84% 10%;
|
||||
--color-green-900: 160 84% 10%;
|
||||
|
||||
--color-blue-50: 217 91% 95%;
|
||||
--color-blue-100: 217 91% 88%;
|
||||
@@ -74,7 +72,7 @@ html, body, #root {
|
||||
--color-blue-800: 217 91% 20%;
|
||||
--color-blue-900: 217 91% 10%;
|
||||
|
||||
--color-pink-50: 292 84% 95%;
|
||||
--color-pink-50: 292 84% 95%;
|
||||
--color-pink-100: 292 84% 88%;
|
||||
--color-pink-200: 292 84% 76%;
|
||||
--color-pink-300: 292 84% 70%;
|
||||
@@ -118,7 +116,7 @@ html, body, #root {
|
||||
--color-orange-800: 25 95% 20%;
|
||||
--color-orange-900: 25 95% 10%;
|
||||
|
||||
--color-yellow-50: 45 93% 95%;
|
||||
--color-yellow-50: 45 93% 95%;
|
||||
--color-yellow-100: 45 93% 88%;
|
||||
--color-yellow-200: 45 93% 76%;
|
||||
--color-yellow-300: 45 93% 70%;
|
||||
@@ -150,8 +148,6 @@ html, body, #root {
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--color-background: 217 21% 7%;
|
||||
|
||||
--color-green-900: 160 84% 95%;
|
||||
--color-green-800: 160 84% 88%;
|
||||
--color-green-700: 160 84% 76%;
|
||||
@@ -161,7 +157,7 @@ html, body, #root {
|
||||
--color-green-300: 160 84% 38%;
|
||||
--color-green-200: 160 84% 30%;
|
||||
--color-green-100: 160 84% 20%;
|
||||
--color-green-50: 160 84% 10%;
|
||||
--color-green-50: 160 84% 10%;
|
||||
|
||||
--color-blue-900: 217 91% 95%;
|
||||
--color-blue-800: 217 91% 88%;
|
||||
@@ -174,7 +170,7 @@ html, body, #root {
|
||||
--color-blue-100: 217 91% 20%;
|
||||
--color-blue-50: 217 91% 10%;
|
||||
|
||||
--color-pink-900: 292 84% 95%;
|
||||
--color-pink-900: 292 84% 95%;
|
||||
--color-pink-800: 292 84% 88%;
|
||||
--color-pink-700: 292 84% 76%;
|
||||
--color-pink-600: 292 84% 70%;
|
||||
@@ -218,7 +214,7 @@ html, body, #root {
|
||||
--color-orange-100: 25 95% 20%;
|
||||
--color-orange-50: 25 95% 10%;
|
||||
|
||||
--color-yellow-900: 45 93% 95%;
|
||||
--color-yellow-900: 45 93% 95%;
|
||||
--color-yellow-800: 45 93% 88%;
|
||||
--color-yellow-700: 45 93% 76%;
|
||||
--color-yellow-600: 45 93% 70%;
|
||||
|
||||
@@ -13,11 +13,11 @@ import { requestsQueryKey } from './hooks/useRequest';
|
||||
import { responsesQueryKey } from './hooks/useResponses';
|
||||
import type { HttpRequest, HttpResponse } from './lib/models';
|
||||
import { convertDates } from './lib/models';
|
||||
import { setTheme } from './lib/theme';
|
||||
import { setAppearance } from './lib/theme/window';
|
||||
import './main.css';
|
||||
import { Workspaces } from './pages/Workspaces';
|
||||
|
||||
setTheme();
|
||||
setAppearance();
|
||||
|
||||
// WASM stuff
|
||||
await init();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ['class', '[data-theme="dark"]'],
|
||||
darkMode: ['class', '[data-appearance="dark"]'],
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src-web/**/*.{js,ts,jsx,tsx}",
|
||||
@@ -17,8 +17,8 @@ module.exports = {
|
||||
},
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
white: 'hsl(var(--color-white) / <alpha-value>)',
|
||||
black: 'hsl(var(--color-black) / <alpha-value>)',
|
||||
white: 'hsl(0 100% 100% / <alpha-value>)',
|
||||
black: 'hsl(0 100% 0% / <alpha-value>)',
|
||||
background: 'hsl(var(--color-background) / <alpha-value>)',
|
||||
placeholder: 'hsl(var(--color-gray-200) / <alpha-value>)',
|
||||
gray: color('gray'),
|
||||
@@ -34,7 +34,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
function color(name) {
|
||||
const map = {
|
||||
return {
|
||||
50: `hsl(var(--color-${name}-50) / <alpha-value>)`,
|
||||
100: `hsl(var(--color-${name}-100) / <alpha-value>)`,
|
||||
200: `hsl(var(--color-${name}-200) / <alpha-value>)`,
|
||||
@@ -45,9 +45,6 @@ function color(name) {
|
||||
700: `hsl(var(--color-${name}-700) / <alpha-value>)`,
|
||||
800: `hsl(var(--color-${name}-800) / <alpha-value>)`,
|
||||
900: `hsl(var(--color-${name}-900) / <alpha-value>)`,
|
||||
}
|
||||
if (name === 'gray') {
|
||||
map[25] = `hsl(var(--color-${name}-25) / <alpha-value>)`;
|
||||
}
|
||||
return map;
|
||||
950: `hsl(var(--color-${name}-950) / <alpha-value>)`,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user