mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-31 14:43:05 +02: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",
|
"preview": "vite preview",
|
||||||
"tauri-dev": "concurrently -n app,rsw \"tauri dev\" \"rsw watch\"",
|
"tauri-dev": "concurrently -n app,rsw \"tauri dev\" \"rsw watch\"",
|
||||||
"tauri-build": "npm run build:icon && tauri build",
|
"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": {
|
"dependencies": {
|
||||||
"@codemirror/commands": "^6.2.1",
|
"@codemirror/commands": "^6.2.1",
|
||||||
@@ -34,6 +36,7 @@
|
|||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"framer-motion": "^9.0.4",
|
"framer-motion": "^9.0.4",
|
||||||
|
"parse-color": "^1.0.0",
|
||||||
"parse-json": "^6.0.2",
|
"parse-json": "^6.0.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -43,6 +46,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.2.2",
|
"@tauri-apps/cli": "^1.2.2",
|
||||||
"@types/node": "^18.7.10",
|
"@types/node": "^18.7.10",
|
||||||
|
"@types/parse-color": "^1.0.1",
|
||||||
"@types/parse-json": "^4.0.0",
|
"@types/parse-json": "^4.0.0",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
@@ -63,6 +67,7 @@
|
|||||||
"typescript": "^4.6.4",
|
"typescript": "^4.6.4",
|
||||||
"vite": "^4.0.0",
|
"vite": "^4.0.0",
|
||||||
"vite-plugin-rsw": "^2.0.11",
|
"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">
|
<div className="grid grid-rows-[auto_1fr] h-full overflow-hidden">
|
||||||
<HStack
|
<HStack
|
||||||
as={WindowDragRegion}
|
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"
|
items="center"
|
||||||
>
|
>
|
||||||
{request.name}
|
{request.name}
|
||||||
</HStack>
|
</HStack>
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'bg-gray-25 grid overflow-auto',
|
'grid overflow-auto',
|
||||||
isH ? 'grid-cols-[1fr_1fr]' : 'grid-rows-[minmax(0,auto)_minmax(0,100%)]',
|
isH ? 'grid-cols-[1fr_1fr]' : 'grid-rows-[minmax(0,auto)_minmax(0,100%)]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<RequestPane
|
<RequestPane
|
||||||
fullHeight={isH}
|
fullHeight={isH}
|
||||||
request={request}
|
request={request}
|
||||||
className={classnames(isH ? 'pr-0' : 'pb-3 mb-1')}
|
className={classnames(!isH && 'pr-2 pb-3 mb-1')}
|
||||||
/>
|
/>
|
||||||
<ResponsePane requestId={request.id} />
|
<ResponsePane requestId={request.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { Icon } from './Icon';
|
|||||||
|
|
||||||
const colorStyles = {
|
const colorStyles = {
|
||||||
default: 'hover:bg-gray-500/10 text-gray-600',
|
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',
|
primary: 'bg-blue-400',
|
||||||
secondary: 'bg-violet-400',
|
secondary: 'bg-violet-400',
|
||||||
warning: 'bg-orange-400',
|
warning: 'bg-orange-400',
|
||||||
@@ -45,7 +46,9 @@ export const Button = forwardRef(function Button<T extends ElementType>(
|
|||||||
type="button"
|
type="button"
|
||||||
className={classnames(
|
className={classnames(
|
||||||
className,
|
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]',
|
// 'active:translate-y-[0.5px] active:scale-[0.99]',
|
||||||
colorStyles[color || 'default'],
|
colorStyles[color || 'default'],
|
||||||
justify === 'start' && 'justify-start',
|
justify === 'start' && 'justify-start',
|
||||||
@@ -57,7 +60,7 @@ export const Button = forwardRef(function Button<T extends ElementType>(
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{forDropdown && <Icon icon="triangle-down" className="ml-1 -mr-1" />}
|
{forDropdown && <Icon icon="triangleDown" className="ml-1 -mr-1" />}
|
||||||
</Component>
|
</Component>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -264,11 +264,7 @@ function DropdownMenuSeparator({ className, ...props }: D.DropdownMenuSeparatorP
|
|||||||
|
|
||||||
function DropdownMenuTrigger({ children, className, ...props }: D.DropdownMenuTriggerProps) {
|
function DropdownMenuTrigger({ children, className, ...props }: D.DropdownMenuTriggerProps) {
|
||||||
return (
|
return (
|
||||||
<D.Trigger
|
<D.Trigger asChild className={classnames(className)} {...props}>
|
||||||
asChild
|
|
||||||
className={classnames(className, 'focus:outline-none focus:border-0 focus:shadow-none')}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</D.Trigger>
|
</D.Trigger>
|
||||||
);
|
);
|
||||||
@@ -290,7 +286,7 @@ const ItemInner = forwardRef<HTMLDivElement, ItemInnerProps>(function ItemInner(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
className,
|
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',
|
!noHover && 'focus:bg-gray-50 focus:text-gray-900 rounded',
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
@apply bg-background w-full block text-[0.85rem];
|
@apply w-full block text-[0.85rem];
|
||||||
|
|
||||||
&.cm-focused {
|
&.cm-focused {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
@@ -26,6 +26,18 @@
|
|||||||
@apply text-placeholder;
|
@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 {
|
.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;
|
@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);
|
text-shadow: 0 0 1px rgba(0, 0, 0, 0.9);
|
||||||
@@ -69,10 +81,6 @@
|
|||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor .cm-gutters {
|
|
||||||
@apply bg-background border-0 text-gray-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor .cm-gutterElement {
|
.cm-editor .cm-gutterElement {
|
||||||
transition: color var(--transition-duration);
|
transition: color var(--transition-duration);
|
||||||
}
|
}
|
||||||
@@ -98,10 +106,6 @@
|
|||||||
@apply text-gray-400 bg-gray-100/20;
|
@apply text-gray-400 bg-gray-100/20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor.cm-focused .cm-gutters {
|
|
||||||
@apply text-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor .cm-foldPlaceholder {
|
.cm-editor .cm-foldPlaceholder {
|
||||||
@apply px-2 border border-gray-200 bg-gray-100;
|
@apply px-2 border border-gray-200 bg-gray-100;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { defaultKeymap } from '@codemirror/commands';
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
import type { Extension } from '@codemirror/state';
|
|
||||||
import { Compartment, EditorState } from '@codemirror/state';
|
import { Compartment, EditorState } from '@codemirror/state';
|
||||||
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { EditorView } from 'codemirror';
|
import { EditorView } from 'codemirror';
|
||||||
import type { CSSProperties, HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import './Editor.css';
|
import './Editor.css';
|
||||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||||
import { singleLineExt } from './singleLine';
|
import { singleLineExt } from './singleLine';
|
||||||
|
|
||||||
export interface EditorProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
export interface EditorProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||||
height?: 'auto' | 'full';
|
heightMode?: 'auto' | 'full';
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
backgroundColor?: string;
|
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
valueKey?: string | number;
|
valueKey?: string | number;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
@@ -25,9 +23,8 @@ export interface EditorProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onCha
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Editor({
|
export default function Editor({
|
||||||
height,
|
heightMode,
|
||||||
contentType,
|
contentType,
|
||||||
backgroundColor,
|
|
||||||
autoFocus,
|
autoFocus,
|
||||||
placeholder,
|
placeholder,
|
||||||
valueKey,
|
valueKey,
|
||||||
@@ -53,6 +50,18 @@ export default function Editor({
|
|||||||
[contentType, ref.current],
|
[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
|
// Create codemirror instance when ref initializes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current === null) return;
|
if (ref.current === null) return;
|
||||||
@@ -69,6 +78,7 @@ export default function Editor({
|
|||||||
parent: ref.current,
|
parent: ref.current,
|
||||||
});
|
});
|
||||||
setCm({ view, langHolder });
|
setCm({ view, langHolder });
|
||||||
|
syncGutterBg();
|
||||||
if (autoFocus && view) view.focus();
|
if (autoFocus && view) view.focus();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Failed to initialize Codemirror', e);
|
console.log('Failed to initialize Codemirror', e);
|
||||||
@@ -76,6 +86,10 @@ export default function Editor({
|
|||||||
return () => view?.destroy();
|
return () => view?.destroy();
|
||||||
}, [ref.current, valueKey]);
|
}, [ref.current, valueKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
syncGutterBg();
|
||||||
|
}, [ref.current, className]);
|
||||||
|
|
||||||
// Update value when valueKey changes
|
// Update value when valueKey changes
|
||||||
// TODO: This would be more efficient but the onChange handler gets fired on update
|
// TODO: This would be more efficient but the onChange handler gets fired on update
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
@@ -98,11 +112,10 @@ export default function Editor({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
className,
|
className,
|
||||||
'cm-wrapper text-base',
|
'cm-wrapper text-base bg-background',
|
||||||
height === 'auto' ? 'cm-auto-height' : 'cm-full-height',
|
heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height',
|
||||||
singleLine ? 'cm-singleline' : 'cm-multiline',
|
singleLine ? 'cm-singleline' : 'cm-multiline',
|
||||||
)}
|
)}
|
||||||
data-color-background="var(--color-gray-50)"
|
|
||||||
{...props}
|
{...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.attributeName], color: 'hsl(var(--color-violet-600))' },
|
||||||
{ tag: [t.attributeValue], color: 'hsl(var(--color-orange-600))' },
|
{ tag: [t.attributeValue], color: 'hsl(var(--color-orange-600))' },
|
||||||
{ tag: [t.string], color: 'hsl(var(--color-yellow-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([
|
// export const defaultHighlightStyle = HighlightStyle.define([
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
CameraIcon,
|
CameraIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
ClockIcon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
Cross2Icon,
|
Cross2Icon,
|
||||||
EyeOpenIcon,
|
EyeOpenIcon,
|
||||||
@@ -15,6 +16,8 @@ import {
|
|||||||
SunIcon,
|
SunIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
TriangleDownIcon,
|
TriangleDownIcon,
|
||||||
|
TriangleLeftIcon,
|
||||||
|
TriangleRightIcon,
|
||||||
UpdateIcon,
|
UpdateIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from '@radix-ui/react-icons';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
@@ -26,13 +29,16 @@ type IconName =
|
|||||||
| 'camera'
|
| 'camera'
|
||||||
| 'gear'
|
| 'gear'
|
||||||
| 'eye'
|
| 'eye'
|
||||||
| 'triangle-down'
|
| 'triangleDown'
|
||||||
| 'paper-plane'
|
| 'triangleLeft'
|
||||||
|
| 'triangleRight'
|
||||||
|
| 'paperPlane'
|
||||||
| 'update'
|
| 'update'
|
||||||
| 'question'
|
| 'question'
|
||||||
| 'check'
|
| 'check'
|
||||||
| 'plus'
|
| 'plus'
|
||||||
| 'plus-circled'
|
| 'plusCircle'
|
||||||
|
| 'clock'
|
||||||
| 'sun'
|
| 'sun'
|
||||||
| 'code'
|
| 'code'
|
||||||
| 'x'
|
| 'x'
|
||||||
@@ -40,13 +46,16 @@ type IconName =
|
|||||||
| 'moon';
|
| 'moon';
|
||||||
|
|
||||||
const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = {
|
const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = {
|
||||||
'paper-plane': PaperPlaneIcon,
|
paperPlane: PaperPlaneIcon,
|
||||||
'triangle-down': TriangleDownIcon,
|
triangleDown: TriangleDownIcon,
|
||||||
plus: PlusIcon,
|
plus: PlusIcon,
|
||||||
'plus-circled': PlusCircledIcon,
|
plusCircle: PlusCircledIcon,
|
||||||
|
clock: ClockIcon,
|
||||||
archive: ArchiveIcon,
|
archive: ArchiveIcon,
|
||||||
camera: CameraIcon,
|
camera: CameraIcon,
|
||||||
check: CheckIcon,
|
check: CheckIcon,
|
||||||
|
triangleLeft: TriangleLeftIcon,
|
||||||
|
triangleRight: TriangleRightIcon,
|
||||||
gear: GearIcon,
|
gear: GearIcon,
|
||||||
home: HomeIcon,
|
home: HomeIcon,
|
||||||
update: UpdateIcon,
|
update: UpdateIcon,
|
||||||
|
|||||||
@@ -5,18 +5,22 @@ import type { ButtonProps } from './Button';
|
|||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
import classnames from 'classnames';
|
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(
|
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
|
||||||
{ icon, spin, ...props }: Props,
|
{ icon, spin, className, iconClassName, ...props }: Props,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Button ref={ref} className="group" {...props}>
|
<Button ref={ref} className={classnames(className, 'group')} {...props}>
|
||||||
<Icon
|
<Icon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
spin={spin}
|
spin={spin}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
|
iconClassName,
|
||||||
'text-gray-700 group-hover:text-gray-900',
|
'text-gray-700 group-hover:text-gray-900',
|
||||||
props.disabled && 'opacity-70',
|
props.disabled && 'opacity-70',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function Input({
|
|||||||
className={classnames(
|
className={classnames(
|
||||||
containerClassName,
|
containerClassName,
|
||||||
'relative w-full rounded-md 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',
|
'border border-gray-50 focus-within:border-blue-400/40',
|
||||||
size === 'md' && 'h-10',
|
size === 'md' && 'h-10',
|
||||||
size === 'sm' && 'h-8',
|
size === 'sm' && 'h-8',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function RequestPane({ fullHeight, request, className }: Props) {
|
|||||||
>
|
>
|
||||||
<div className="pl-2">
|
<div className="pl-2">
|
||||||
<UrlBar
|
<UrlBar
|
||||||
className="border-0 mb-1"
|
className="bg-transparent"
|
||||||
key={request.id}
|
key={request.id}
|
||||||
method={request.method}
|
method={request.method}
|
||||||
url={request.url}
|
url={request.url}
|
||||||
@@ -36,28 +36,27 @@ export function RequestPane({ fullHeight, request, className }: Props) {
|
|||||||
{/*<Divider className="mb-2" />*/}
|
{/*<Divider className="mb-2" />*/}
|
||||||
<ScrollArea className="max-w-full pb-2 mx-2">
|
<ScrollArea className="max-w-full pb-2 mx-2">
|
||||||
<HStack className="mt-2 hide-scrollbar" space={1}>
|
<HStack className="mt-2 hide-scrollbar" space={1}>
|
||||||
{['JSON', 'Params', 'Headers', 'Auth', 'Docs'].map((label, i) => (
|
{['JSON', 'Params', 'Headers', 'Auth'].map((label, i) => (
|
||||||
<Button
|
<Button
|
||||||
key={label}
|
key={label}
|
||||||
size="xs"
|
size="xs"
|
||||||
color={i === 0 && 'gray'}
|
color={i === 0 && 'gray'}
|
||||||
className={i !== 0 && 'opacity-50 hover:opacity-60'}
|
className={i !== 0 && 'opacity-80 hover:opacity-100'}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</HStack>
|
</HStack>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<div className="px-0">
|
<Editor
|
||||||
<Editor
|
className="mt-1 !bg-gray-50"
|
||||||
height={fullHeight ? 'full' : 'auto'}
|
heightMode={fullHeight ? 'full' : 'auto'}
|
||||||
valueKey={request.id}
|
valueKey={request.id}
|
||||||
useTemplating
|
useTemplating
|
||||||
defaultValue={request.body ?? ''}
|
defaultValue={request.body ?? ''}
|
||||||
contentType="application/json"
|
contentType="application/json"
|
||||||
onChange={(body) => updateRequest.mutate({ body })}
|
onChange={(body) => updateRequest.mutate({ body })}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function ResponsePane({ requestId, className }: Props) {
|
|||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
className,
|
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">*/}
|
{/*<HStack as={WindowDragRegion} items="center" className="pl-1.5 pr-1">*/}
|
||||||
@@ -58,7 +58,7 @@ export function ResponsePane({ requestId, className }: Props) {
|
|||||||
<>
|
<>
|
||||||
<HStack
|
<HStack
|
||||||
items="center"
|
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">
|
<div className="whitespace-nowrap">
|
||||||
{response.status}
|
{response.status}
|
||||||
@@ -68,7 +68,7 @@ export function ResponsePane({ requestId, className }: Props) {
|
|||||||
{Math.round(response.body.length / 1000)} KB
|
{Math.round(response.body.length / 1000)} KB
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HStack items="center" className="ml-auto">
|
<HStack items="center" className="ml-auto h-8">
|
||||||
{contentType.includes('html') && (
|
{contentType.includes('html') && (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={viewMode === 'pretty' ? 'eye' : 'code'}
|
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>
|
</Dropdown>
|
||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -113,7 +118,7 @@ export function ResponsePane({ requestId, className }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
) : response?.body ? (
|
) : response?.body ? (
|
||||||
<Editor
|
<Editor
|
||||||
backgroundColor="red"
|
className="mr-1 !bg-gray-100"
|
||||||
valueKey={`${contentType}:${response.body}`}
|
valueKey={`${contentType}:${response.body}`}
|
||||||
defaultValue={response?.body}
|
defaultValue={response?.body}
|
||||||
contentType={contentType}
|
contentType={contentType}
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|||||||
|
|
||||||
export function Sidebar({ className, activeRequestId, workspaceId, requests, ...props }: Props) {
|
export function Sidebar({ className, activeRequestId, workspaceId, requests, ...props }: Props) {
|
||||||
const createRequest = useRequestCreate({ workspaceId, navigateAfter: true });
|
const createRequest = useRequestCreate({ workspaceId, navigateAfter: true });
|
||||||
const { toggleTheme } = useTheme();
|
const { appearance, toggleAppearance } = useTheme();
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
{...props}
|
||||||
>
|
>
|
||||||
<HStack as={WindowDragRegion} items="center" justify="end">
|
<HStack as={WindowDragRegion} items="center" justify="end">
|
||||||
@@ -34,17 +37,14 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
|
|||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
{/*<IconButton*/}
|
|
||||||
{/* size="sm"*/}
|
|
||||||
{/* icon="camera"*/}
|
|
||||||
{/* onClick={() => {*/}
|
|
||||||
{/* setOpen((v) => !v);*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/*/>*/}
|
|
||||||
<IconButton size="sm" icon="sun" onClick={toggleTheme} />
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
icon="plus-circled"
|
icon={appearance === 'dark' ? 'moon' : 'sun'}
|
||||||
|
onClick={toggleAppearance}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
icon="plusCircle"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await createRequest.mutate({ name: 'Test Request' });
|
await createRequest.mutate({ name: 'Test Request' });
|
||||||
}}
|
}}
|
||||||
@@ -54,6 +54,19 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
|
|||||||
{requests.map((r) => (
|
{requests.map((r) => (
|
||||||
<SidebarItem key={r.id} request={r} active={r.id === activeRequestId} />
|
<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>
|
</VStack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -64,12 +77,13 @@ function SidebarItem({ request, active }: { request: HttpRequest; active: boolea
|
|||||||
<li key={request.id}>
|
<li key={request.id}>
|
||||||
<Button
|
<Button
|
||||||
as={Link}
|
as={Link}
|
||||||
|
color="tint"
|
||||||
to={`/workspaces/${request.workspaceId}/requests/${request.id}`}
|
to={`/workspaces/${request.workspaceId}/requests/${request.id}`}
|
||||||
className={classnames('w-full', active && 'bg-gray-500/[0.1] text-gray-900')}
|
className={classnames('w-full', active && 'bg-gray-500/[0.1] text-gray-900')}
|
||||||
size="xs"
|
size="xs"
|
||||||
justify="start"
|
justify="start"
|
||||||
>
|
>
|
||||||
{request.name}
|
{request.name || request.url}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ export function UrlBar({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
size="sm"
|
size="xs"
|
||||||
className="ml-1 mr-2 !px-2 !text-gray-800"
|
className="mx-0.5 !text-gray-800"
|
||||||
justify="start"
|
justify="start"
|
||||||
>
|
>
|
||||||
{method.toUpperCase()}
|
{method.toUpperCase()}
|
||||||
@@ -69,11 +69,11 @@ export function UrlBar({
|
|||||||
rightSlot={
|
rightSlot={
|
||||||
<IconButton
|
<IconButton
|
||||||
type="submit"
|
type="submit"
|
||||||
size="sm"
|
className="mr-0.5"
|
||||||
icon={loading ? 'update' : 'paper-plane'}
|
size="xs"
|
||||||
|
icon={loading ? 'update' : 'paperPlane'}
|
||||||
spin={loading}
|
spin={loading}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="mx-1 !px-4"
|
|
||||||
title="Send Request"
|
title="Send Request"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,38 @@
|
|||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useEffect } from 'react';
|
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(() => {
|
useEffect(() => {
|
||||||
if (!subscribeToChanges) return;
|
return subscribeToPreferredAppearanceChange(themeChange);
|
||||||
const unsub = subscribeToPreferredThemeChange(setTheme);
|
}, []);
|
||||||
return unsub;
|
|
||||||
}, [subscribeToChanges]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toggleTheme: toggleTheme,
|
appearance: appearance.data,
|
||||||
|
toggleAppearance: handleToggleAppearance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export interface HttpResponse extends BaseModel {
|
|||||||
requestId: string;
|
requestId: string;
|
||||||
body: string;
|
body: string;
|
||||||
error: string;
|
error: string;
|
||||||
status: string;
|
status: number;
|
||||||
elapsed: number;
|
elapsed: number;
|
||||||
statusReason: string;
|
statusReason: string;
|
||||||
url: 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 {
|
@layer base {
|
||||||
:root, [data-theme="light"] {
|
:root {
|
||||||
/* Colors */
|
|
||||||
--color-white: 255 100% 100%;
|
--color-white: 255 100% 100%;
|
||||||
--color-black: 255 0% 0%;
|
--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-50: 160 84% 95%;
|
||||||
--color-green-100: 160 84% 88%;
|
--color-green-100: 160 84% 88%;
|
||||||
--color-green-200: 160 84% 76%;
|
--color-green-200: 160 84% 76%;
|
||||||
@@ -61,7 +59,7 @@ html, body, #root {
|
|||||||
--color-green-600: 160 84% 38%;
|
--color-green-600: 160 84% 38%;
|
||||||
--color-green-700: 160 84% 30%;
|
--color-green-700: 160 84% 30%;
|
||||||
--color-green-800: 160 84% 20%;
|
--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-50: 217 91% 95%;
|
||||||
--color-blue-100: 217 91% 88%;
|
--color-blue-100: 217 91% 88%;
|
||||||
@@ -74,7 +72,7 @@ html, body, #root {
|
|||||||
--color-blue-800: 217 91% 20%;
|
--color-blue-800: 217 91% 20%;
|
||||||
--color-blue-900: 217 91% 10%;
|
--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-100: 292 84% 88%;
|
||||||
--color-pink-200: 292 84% 76%;
|
--color-pink-200: 292 84% 76%;
|
||||||
--color-pink-300: 292 84% 70%;
|
--color-pink-300: 292 84% 70%;
|
||||||
@@ -118,7 +116,7 @@ html, body, #root {
|
|||||||
--color-orange-800: 25 95% 20%;
|
--color-orange-800: 25 95% 20%;
|
||||||
--color-orange-900: 25 95% 10%;
|
--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-100: 45 93% 88%;
|
||||||
--color-yellow-200: 45 93% 76%;
|
--color-yellow-200: 45 93% 76%;
|
||||||
--color-yellow-300: 45 93% 70%;
|
--color-yellow-300: 45 93% 70%;
|
||||||
@@ -150,8 +148,6 @@ html, body, #root {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
--color-background: 217 21% 7%;
|
|
||||||
|
|
||||||
--color-green-900: 160 84% 95%;
|
--color-green-900: 160 84% 95%;
|
||||||
--color-green-800: 160 84% 88%;
|
--color-green-800: 160 84% 88%;
|
||||||
--color-green-700: 160 84% 76%;
|
--color-green-700: 160 84% 76%;
|
||||||
@@ -161,7 +157,7 @@ html, body, #root {
|
|||||||
--color-green-300: 160 84% 38%;
|
--color-green-300: 160 84% 38%;
|
||||||
--color-green-200: 160 84% 30%;
|
--color-green-200: 160 84% 30%;
|
||||||
--color-green-100: 160 84% 20%;
|
--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-900: 217 91% 95%;
|
||||||
--color-blue-800: 217 91% 88%;
|
--color-blue-800: 217 91% 88%;
|
||||||
@@ -174,7 +170,7 @@ html, body, #root {
|
|||||||
--color-blue-100: 217 91% 20%;
|
--color-blue-100: 217 91% 20%;
|
||||||
--color-blue-50: 217 91% 10%;
|
--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-800: 292 84% 88%;
|
||||||
--color-pink-700: 292 84% 76%;
|
--color-pink-700: 292 84% 76%;
|
||||||
--color-pink-600: 292 84% 70%;
|
--color-pink-600: 292 84% 70%;
|
||||||
@@ -218,7 +214,7 @@ html, body, #root {
|
|||||||
--color-orange-100: 25 95% 20%;
|
--color-orange-100: 25 95% 20%;
|
||||||
--color-orange-50: 25 95% 10%;
|
--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-800: 45 93% 88%;
|
||||||
--color-yellow-700: 45 93% 76%;
|
--color-yellow-700: 45 93% 76%;
|
||||||
--color-yellow-600: 45 93% 70%;
|
--color-yellow-600: 45 93% 70%;
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import { requestsQueryKey } from './hooks/useRequest';
|
|||||||
import { responsesQueryKey } from './hooks/useResponses';
|
import { responsesQueryKey } from './hooks/useResponses';
|
||||||
import type { HttpRequest, HttpResponse } from './lib/models';
|
import type { HttpRequest, HttpResponse } from './lib/models';
|
||||||
import { convertDates } from './lib/models';
|
import { convertDates } from './lib/models';
|
||||||
import { setTheme } from './lib/theme';
|
import { setAppearance } from './lib/theme/window';
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import { Workspaces } from './pages/Workspaces';
|
import { Workspaces } from './pages/Workspaces';
|
||||||
|
|
||||||
setTheme();
|
setAppearance();
|
||||||
|
|
||||||
// WASM stuff
|
// WASM stuff
|
||||||
await init();
|
await init();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ['class', '[data-theme="dark"]'],
|
darkMode: ['class', '[data-appearance="dark"]'],
|
||||||
content: [
|
content: [
|
||||||
"./index.html",
|
"./index.html",
|
||||||
"./src-web/**/*.{js,ts,jsx,tsx}",
|
"./src-web/**/*.{js,ts,jsx,tsx}",
|
||||||
@@ -17,8 +17,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
transparent: 'transparent',
|
transparent: 'transparent',
|
||||||
white: 'hsl(var(--color-white) / <alpha-value>)',
|
white: 'hsl(0 100% 100% / <alpha-value>)',
|
||||||
black: 'hsl(var(--color-black) / <alpha-value>)',
|
black: 'hsl(0 100% 0% / <alpha-value>)',
|
||||||
background: 'hsl(var(--color-background) / <alpha-value>)',
|
background: 'hsl(var(--color-background) / <alpha-value>)',
|
||||||
placeholder: 'hsl(var(--color-gray-200) / <alpha-value>)',
|
placeholder: 'hsl(var(--color-gray-200) / <alpha-value>)',
|
||||||
gray: color('gray'),
|
gray: color('gray'),
|
||||||
@@ -34,7 +34,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function color(name) {
|
function color(name) {
|
||||||
const map = {
|
return {
|
||||||
50: `hsl(var(--color-${name}-50) / <alpha-value>)`,
|
50: `hsl(var(--color-${name}-50) / <alpha-value>)`,
|
||||||
100: `hsl(var(--color-${name}-100) / <alpha-value>)`,
|
100: `hsl(var(--color-${name}-100) / <alpha-value>)`,
|
||||||
200: `hsl(var(--color-${name}-200) / <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>)`,
|
700: `hsl(var(--color-${name}-700) / <alpha-value>)`,
|
||||||
800: `hsl(var(--color-${name}-800) / <alpha-value>)`,
|
800: `hsl(var(--color-${name}-800) / <alpha-value>)`,
|
||||||
900: `hsl(var(--color-${name}-900) / <alpha-value>)`,
|
900: `hsl(var(--color-${name}-900) / <alpha-value>)`,
|
||||||
}
|
950: `hsl(var(--color-${name}-950) / <alpha-value>)`,
|
||||||
if (name === 'gray') {
|
};
|
||||||
map[25] = `hsl(var(--color-${name}-25) / <alpha-value>)`;
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user