A bunch more theme stuff

This commit is contained in:
Gregory Schier
2024-05-22 23:14:53 -07:00
parent 83aaeb94f6
commit 8e662e6feb
61 changed files with 8374 additions and 530 deletions

View File

@@ -1,7 +1,5 @@
import { useQueryClient } from '@tanstack/react-query';
import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useCommandPalette } from '../hooks/useCommandPalette';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { foldersQueryKey } from '../hooks/useFolders';
@@ -19,12 +17,11 @@ import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { settingsQueryKey } from '../hooks/useSettings';
import { useSyncAppearance } from '../hooks/useSyncAppearance';
import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument';
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
import { workspacesQueryKey } from '../hooks/useWorkspaces';
import type { Model } from '../lib/models';
import { modelsEq } from '../lib/models';
import { setPathname } from '../lib/persistPathname';
const DEFAULT_FONT_SIZE = 16;
@@ -35,7 +32,7 @@ export function GlobalHooks() {
useRecentRequests();
// Other useful things
useSyncAppearance();
useSyncThemeToDocument();
useSyncWindowTitle();
useGlobalCommands();
useCommandPalette();
@@ -44,12 +41,6 @@ export function GlobalHooks() {
const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
// Listen for location changes and update the pathname
const location = useLocation();
useEffect(() => {
setPathname(location.pathname).catch(console.error);
}, [location.pathname]);
interface ModelPayload {
model: Model;
windowLabel: string;

View File

@@ -0,0 +1,15 @@
import type { ReactNode } from 'react';
import { useAppInfo } from '../hooks/useAppInfo';
interface Props {
children: ReactNode;
}
export function IsDev({ children }: Props) {
const appInfo = useAppInfo();
if (!appInfo?.isDev) {
return null;
}
return <>{children}</>;
}

View File

@@ -20,9 +20,11 @@ import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
import { ResponseHeaders } from './ResponseHeaders';
import { AudioViewer } from './responseViewers/AudioViewer';
import { CsvViewer } from './responseViewers/CsvViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { TextViewer } from './responseViewers/TextViewer';
import { VideoViewer } from './responseViewers/VideoViewer';
import { WebPageViewer } from './responseViewers/WebPageViewer';
interface Props {
@@ -76,7 +78,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
'x-theme-responsePane',
'max-h-full h-full',
'bg-background rounded-md border border-background-highlight',
'shadow relative',
'relative',
)}
>
{activeResponse == null ? (
@@ -154,6 +156,10 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
</div>
) : contentType?.startsWith('image') ? (
<ImageViewer className="pb-2" response={activeResponse} />
) : contentType?.startsWith('audio') ? (
<AudioViewer response={activeResponse} />
) : contentType?.startsWith('video') ? (
<VideoViewer response={activeResponse} />
) : activeResponse.contentLength > 2 * 1000 * 1000 ? (
<EmptyStateText>Cannot preview text responses larger than 2MB</EmptyStateText>
) : viewMode === 'pretty' && contentType?.includes('html') ? (
@@ -161,7 +167,11 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
) : contentType?.match(/csv|tab-separated/) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : (
<TextViewer response={activeResponse} pretty={viewMode === 'pretty'} />
<TextViewer
className="-mr-2" // Pull to the right
response={activeResponse}
pretty={viewMode === 'pretty'}
/>
)}
</TabContent>
</Tabs>

View File

@@ -0,0 +1,162 @@
import React from 'react';
import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
import { useSettings } from '../../hooks/useSettings';
import { useThemes } from '../../hooks/useThemes';
import { useUpdateSettings } from '../../hooks/useUpdateSettings';
import { trackEvent } from '../../lib/analytics';
import { isThemeDark } from '../../lib/theme/window';
import type { ButtonProps } from '../core/Button';
import { Editor } from '../core/Editor';
import type { IconProps } from '../core/Icon';
import { IconButton } from '../core/IconButton';
import type { SelectOption } from '../core/Select';
import { Select } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
const buttonColors: ButtonProps['color'][] = [
'primary',
'info',
'success',
'notice',
'warning',
'danger',
'secondary',
'default',
];
const icons: IconProps['icon'][] = [
'info',
'box',
'update',
'alert',
'arrowBigRightDash',
'download',
'copy',
'magicWand',
'settings',
'trash',
'sparkles',
'pencil',
'paste',
'search',
'sendHorizontal',
];
export function SettingsAppearance() {
const workspace = useActiveWorkspace();
const settings = useSettings();
const updateSettings = useUpdateSettings();
const appearance = useResolvedAppearance();
const { themes } = useThemes();
const activeTheme = useResolvedTheme();
if (settings == null || workspace == null) {
return null;
}
const lightThemes: SelectOption<string>[] = themes
.filter((theme) => !isThemeDark(theme))
.map((theme) => ({
label: theme.name,
value: theme.id,
}));
const darkThemes: SelectOption<string>[] = themes
.filter((theme) => isThemeDark(theme))
.map((theme) => ({
label: theme.name,
value: theme.id,
}));
return (
<VStack space={2} className="mb-4">
<Select
name="appearance"
label="Appearance"
labelPosition="left"
size="sm"
value={settings.appearance}
onChange={async (appearance) => {
await updateSettings.mutateAsync({ ...settings, appearance });
trackEvent('setting', 'update', { appearance });
}}
options={[
{ label: 'Sync with OS', value: 'system' },
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
]}
/>
<div className="grid grid-cols-2 gap-3">
<Select
name="lightTheme"
label={'Light Theme' + (appearance !== 'dark' ? ' (active)' : '')}
labelPosition="top"
size="sm"
value={activeTheme.light.id}
options={lightThemes}
onChange={async (themeLight) => {
await updateSettings.mutateAsync({ ...settings, themeLight });
trackEvent('setting', 'update', { themeLight });
}}
/>
<Select
name="darkTheme"
label={'Dark Theme' + (appearance === 'dark' ? ' (active)' : '')}
labelPosition="top"
size="sm"
value={activeTheme.dark.id}
options={darkThemes}
onChange={async (themeDark) => {
await updateSettings.mutateAsync({ ...settings, themeDark });
trackEvent('setting', 'update', { themeDark });
}}
/>
</div>
<VStack
space={3}
className="mt-3 w-full bg-background p-3 border border-dashed border-background-highlight rounded"
>
<div className="text-sm text-fg font-bold">
Theme Preview <span className="text-fg-subtle">({appearance})</span>
</div>
<HStack space={1.5} alignItems="center" className="w-full">
{buttonColors.map((c, i) => (
<IconButton
key={c}
color={c}
size="2xs"
iconSize="xs"
icon={icons[i % icons.length]!}
iconClassName="text-fg"
title={`${c}`}
/>
))}
{buttonColors.map((c, i) => (
<IconButton
key={c}
color={c}
variant="border"
size="2xs"
iconSize="xs"
icon={icons[i % icons.length]!}
iconClassName="text-fg"
title={`${c}`}
/>
))}
</HStack>
<Editor
defaultValue={[
'let foo = { // Demo code editor',
' foo: ("bar" || "baz" ?? \'qux\'),',
' baz: [1, 10.2, null, false, true],',
'};',
].join('\n')}
heightMode="auto"
contentType="application/javascript"
/>
</VStack>
</VStack>
);
}

View File

@@ -0,0 +1,103 @@
import React from 'react';
import { capitalize } from '../../lib/capitalize';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
import { Editor } from '../core/Editor';
import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
import { Input } from '../core/Input';
const buttonColors = [
'primary',
'secondary',
'info',
'success',
'warning',
'danger',
'default',
] as const;
const icons: IconProps['icon'][] = [
'info',
'box',
'update',
'alert',
'arrowBigRightDash',
'download',
'copy',
'magicWand',
'settings',
'trash',
'sparkles',
'pencil',
'paste',
'search',
'sendHorizontal',
];
export function SettingsDesign() {
return (
<div className="p-2 flex flex-col gap-3">
<Input
label="Field Label"
name="demo"
placeholder="Placeholder"
size="sm"
rightSlot={<IconButton title="search" size="xs" className="w-8 m-0.5" icon="search" />}
/>
<Editor
defaultValue={[
'// Demo code editor',
'let foo = {',
' foo: ("bar" || "baz" ?? \'qux\'),',
' baz: [1, 10.2, null, false, true],',
'};',
].join('\n')}
heightMode="auto"
contentType="application/javascript"
/>
<div className="flex flex-col gap-1">
<div className="flex flex-wrap gap-1">
{buttonColors.map((c, i) => (
<Button key={c} color={c} size="sm" leftSlot={<Icon size="sm" icon={icons[i]!} />}>
{capitalize(c).slice(0, 4)}
</Button>
))}
</div>
<div className="flex flex-wrap gap-1">
{buttonColors.map((c, i) => (
<Button
key={c}
color={c}
variant="border"
size="sm"
leftSlot={<Icon size="sm" icon={icons[i]!} />}
>
{capitalize(c).slice(0, 4)}
</Button>
))}
</div>
<div className="flex gap-1">
{icons.map((v, i) => (
<IconButton
color={buttonColors[i % buttonColors.length]}
title={v}
variant="border"
size="sm"
key={v}
icon={v}
/>
))}
</div>
</div>
<div className="flex flex-col gap-1">
<Banner color="primary">Primary banner</Banner>
<Banner color="secondary">Secondary banner</Banner>
<Banner color="danger">Danger banner</Banner>
<Banner color="warning">Warning banner</Banner>
<Banner color="success">Success banner</Banner>
</div>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import classNames from 'classnames';
import { createGlobalState } from 'react-use';
import { useAppInfo } from '../../hooks/useAppInfo';
import { capitalize } from '../../lib/capitalize';
import { TabContent, Tabs } from '../core/Tabs/Tabs';
import { SettingsAppearance } from './SettingsAppearance';
import { SettingsDesign } from './SettingsDesign';
import { SettingsGeneral } from './SettingsGeneral';
enum Tab {
General = 'general',
Appearance = 'appearance',
// Dev-only
Design = 'design',
}
const tabs = [Tab.General, Tab.Appearance, Tab.Design];
const useTabState = createGlobalState<string>(Tab.Appearance);
export const SettingsDialog = () => {
const [tab, setTab] = useTabState();
const appInfo = useAppInfo();
const isDev = appInfo?.isDev ?? false;
return (
<div className={classNames('w-[70vw] max-w-[40rem]', 'h-[80vh]')}>
<Tabs
value={tab}
addBorders
label="Settings"
tabListClassName="h-md !-ml-1 mt-2"
onChangeValue={setTab}
tabs={tabs
.filter((t) => t !== Tab.Design || isDev)
.map((value) => ({ value, label: capitalize(value) }))}
>
<TabContent value={Tab.General} className="pt-3 overflow-y-auto h-full px-4">
<SettingsGeneral />
</TabContent>
<TabContent value={Tab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
<SettingsAppearance />
</TabContent>
<TabContent value={Tab.Design} className="pt-3 overflow-y-auto h-full px-4">
<SettingsDesign />
</TabContent>
</Tabs>
</div>
);
};

View File

@@ -1,20 +1,20 @@
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppInfo } from '../hooks/useAppInfo';
import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
import { useSettings } from '../hooks/useSettings';
import { useUpdateSettings } from '../hooks/useUpdateSettings';
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
import { trackEvent } from '../lib/analytics';
import { Checkbox } from './core/Checkbox';
import { Heading } from './core/Heading';
import { IconButton } from './core/IconButton';
import { Input } from './core/Input';
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
import { Select } from './core/Select';
import { Separator } from './core/Separator';
import { VStack } from './core/Stacks';
import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
import { useAppInfo } from '../../hooks/useAppInfo';
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates';
import { useSettings } from '../../hooks/useSettings';
import { useUpdateSettings } from '../../hooks/useUpdateSettings';
import { useUpdateWorkspace } from '../../hooks/useUpdateWorkspace';
import { trackEvent } from '../../lib/analytics';
import { Checkbox } from '../core/Checkbox';
import { Heading } from '../core/Heading';
import { IconButton } from '../core/IconButton';
import { Input } from '../core/Input';
import { KeyValueRow, KeyValueRows } from '../core/KeyValueRow';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import { VStack } from '../core/Stacks';
export const SettingsDialog = () => {
export function SettingsGeneral() {
const workspace = useActiveWorkspace();
const updateWorkspace = useUpdateWorkspace(workspace?.id ?? null);
const settings = useSettings();
@@ -28,32 +28,6 @@ export const SettingsDialog = () => {
return (
<VStack space={2} className="mb-4">
<Select
name="appearance"
label="Appearance"
labelPosition="left"
size="sm"
value={settings.appearance}
onChange={async (appearance) => {
await updateSettings.mutateAsync({ ...settings, appearance });
trackEvent('setting', 'update', { appearance });
}}
options={[
{
label: 'System',
value: 'system',
},
{
label: 'Light',
value: 'light',
},
{
label: 'Dark',
value: 'dark',
},
]}
/>
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1">
<Select
name="updateChannel"
@@ -133,10 +107,10 @@ export const SettingsDialog = () => {
<Heading size={2}>App Info</Heading>
<KeyValueRows>
<KeyValueRow label="Version" value={appInfo.data?.version} />
<KeyValueRow label="Data Directory" value={appInfo.data?.appDataDir} />
<KeyValueRow label="Logs Directory" value={appInfo.data?.appLogDir} />
<KeyValueRow label="Version" value={appInfo?.version} />
<KeyValueRow label="Data Directory" value={appInfo?.appDataDir} />
<KeyValueRow label="Logs Directory" value={appInfo?.appLogDir} />
</KeyValueRows>
</VStack>
);
};
}

View File

@@ -11,7 +11,7 @@ import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { useDialog } from './DialogContext';
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
import { SettingsDialog } from './SettingsDialog';
import { SettingsDialog } from './Settings/SettingsDialog';
export function SettingsDropdown() {
const importData = useImportData();
@@ -24,8 +24,9 @@ export function SettingsDropdown() {
const showSettings = () => {
dialog.show({
id: 'settings',
size: 'md',
title: 'Settings',
size: 'dynamic',
noScroll: true,
noPadding: true,
render: () => <SettingsDialog />,
});
};
@@ -69,7 +70,7 @@ export function SettingsDropdown() {
leftSlot: <Icon icon="folderOutput" />,
onSelect: () => exportData.mutate(),
},
{ type: 'separator', label: `Yaak v${appInfo.data?.version}` },
{ type: 'separator', label: `Yaak v${appInfo?.version}` },
{
key: 'update-check',
label: 'Check for Updates',
@@ -88,7 +89,7 @@ export function SettingsDropdown() {
label: 'Changelog',
leftSlot: <Icon icon="cake" />,
rightSlot: <Icon icon="externalLink" />,
onSelect: () => open(`https://yaak.app/changelog/${appInfo.data?.version}`),
onSelect: () => open(`https://yaak.app/changelog/${appInfo?.version}`),
},
]}
>

View File

@@ -468,47 +468,6 @@ export function Sidebar({ className }: Props) {
handleEnd={handleEnd}
handleDragStart={handleDragStart}
/>
{/*<div className="p-2 flex flex-col gap-1">*/}
{/* <div className="flex flex-wrap gap-1">*/}
{/* <Button color="primary">Primary</Button>*/}
{/* <Button color="secondary">Secondary</Button>*/}
{/* <Button color="info">Info</Button>*/}
{/* <Button color="success">Success</Button>*/}
{/* <Button color="warning">Warning</Button>*/}
{/* <Button color="danger">Danger</Button>*/}
{/* <Button color="default">Default</Button>*/}
{/* </div>*/}
{/* <div className="flex flex-wrap gap-1">*/}
{/* <Button variant="border" color="primary">*/}
{/* Primary*/}
{/* </Button>*/}
{/* <Button variant="border" color="secondary">*/}
{/* Secondary*/}
{/* </Button>*/}
{/* <Button variant="border" color="info">*/}
{/* Info*/}
{/* </Button>*/}
{/* <Button variant="border" color="success">*/}
{/* Success*/}
{/* </Button>*/}
{/* <Button variant="border" color="warning">*/}
{/* Warning*/}
{/* </Button>*/}
{/* <Button variant="border" color="danger">*/}
{/* Danger*/}
{/* </Button>*/}
{/* <Button variant="border" color="default">*/}
{/* Default*/}
{/* </Button>*/}
{/* </div>*/}
{/* <div className="flex flex-col gap-1">*/}
{/* <Banner color="primary">Primary banner</Banner>*/}
{/* <Banner color="secondary">Secondary banner</Banner>*/}
{/* <Banner color="danger">Danger banner</Banner>*/}
{/* <Banner color="warning">Warning banner</Banner>*/}
{/* <Banner color="success">Success banner</Banner>*/}
{/* </div>*/}
{/*</div>*/}
</aside>
);
}
@@ -800,11 +759,14 @@ const SidebarItem = forwardRef(function SidebarItem(
const name = await prompt({
id: 'rename-request',
title: 'Rename Request',
description: (
<>
Enter a new name for <InlineCode>{itemName}</InlineCode>
</>
),
description:
itemName === '' ? (
'Enter a new name'
) : (
<>
Enter a new name for <InlineCode>{itemName}</InlineCode>
</>
),
name: 'name',
label: 'Name',
placeholder: 'New Name',
@@ -893,7 +855,7 @@ const SidebarItem = forwardRef(function SidebarItem(
{isResponseLoading(latestHttpResponse) ? (
<Icon spin size="sm" icon="refresh" className="text-fg-subtler" />
) : (
<StatusTag className="text-2xs dark:opacity-80" response={latestHttpResponse} />
<StatusTag className="text-2xs" response={latestHttpResponse} />
)}
</div>
) : null}

View File

@@ -59,7 +59,7 @@ export const UrlBar = memo(function UrlBar({
};
return (
<form onSubmit={handleSubmit} className={classNames(className)}>
<form onSubmit={handleSubmit} className={classNames('x-theme-urlBar', className)}>
<Input
autocompleteVariables
ref={inputRef}
@@ -76,7 +76,7 @@ export const UrlBar = memo(function UrlBar({
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
onPaste={onPaste}
containerClassName="shadow bg-background border"
containerClassName="bg-background border border-background-highlight"
onChange={onUrlChange}
defaultValue={url}
placeholder={placeholder}

View File

@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
interface Props {
children: ReactNode;
className?: string;
color?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger';
}
export function Banner({ children, className, color = 'secondary' }: Props) {

View File

@@ -14,11 +14,12 @@ export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
| 'primary'
| 'info'
| 'success'
| 'notice'
| 'warning'
| 'danger';
variant?: 'border' | 'solid';
isLoading?: boolean;
size?: 'xs' | 'sm' | 'md';
size?: '2xs' | 'xs' | 'sm' | 'md';
justify?: 'start' | 'center';
type?: 'button' | 'submit';
forDropdown?: boolean;
@@ -60,24 +61,27 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
`x-theme-button--${variant}`,
`x-theme-button--${variant}--${color}`,
'text-fg',
'border', // They all have borders to ensure the same width
'max-w-full min-w-0', // Help with truncation
'hocus:opacity-100', // Force opacity for certain hover effects
'whitespace-nowrap outline-none',
'flex-shrink-0 flex items-center',
'focus-visible-or-class:ring rounded-md',
'focus-visible-or-class:ring',
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
justify === 'start' && 'justify-start',
justify === 'center' && 'justify-center',
size === 'md' && 'h-md px-3',
size === 'sm' && 'h-sm px-2.5 text-sm',
size === 'xs' && 'h-xs px-2 text-sm',
size === 'md' && 'h-md px-3 rounded-md',
size === 'sm' && 'h-sm px-2.5 text-sm rounded-md',
size === 'xs' && 'h-xs px-2 text-sm rounded-md',
size === '2xs' && 'h-5 px-1 text-xs rounded',
// Solids
variant === 'solid' && 'border-transparent',
variant === 'solid' && color === 'custom' && 'ring-blue-400',
variant === 'solid' &&
color !== 'custom' &&
color !== 'default' &&
'bg-background enabled:hocus:bg-background-highlight ring-background-highlight-secondary',
variant === 'solid' && color === 'custom' && 'ring-blue-400',
variant === 'solid' &&
color === 'default' &&
'enabled:hocus:bg-background-highlight ring-fg-info',

View File

@@ -30,7 +30,7 @@ export function Checkbox({
alignItems="center"
className={classNames(className, 'text-fg text-sm', disabled && 'opacity-disabled')}
>
<div className={classNames(inputWrapperClassName, 'relative flex')}>
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}>
<input
aria-hidden
className={classNames(

View File

@@ -94,7 +94,7 @@ export function Dialog({
className={classNames(
'h-full w-full grid grid-cols-[minmax(0,1fr)]',
!noPadding && 'px-6 py-2',
!noScroll && 'overflow-y-auto',
!noScroll && 'overflow-y-auto overflow-x-hidden',
)}
>
{children}

View File

@@ -413,7 +413,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
)}
{isOpen && (
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
<div className="x-theme-dialog">
<div className="x-theme-menu">
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={handleClose} />
<motion.div
tabIndex={0}

View File

@@ -7,7 +7,7 @@
.cm-cursor {
@apply border-fg !important;
/* Widen the cursor */
@apply border-l-2;
@apply border-l-[2px];
}
&.cm-focused {

View File

@@ -283,7 +283,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
}
return (
<div className="group relative h-full w-full">
<div className="group relative h-full w-full x-theme-editor bg-background">
{cmContainer}
{decoratedActions && (
<HStack

View File

@@ -46,45 +46,23 @@ export const myHighlightStyle = HighlightStyle.define([
fontStyle: 'italic',
},
{
tag: [t.paren],
tag: [t.paren, t.bracket, t.brace],
color: 'var(--fg)',
},
{
tag: [t.name, t.tagName, t.angleBracket, t.docString, t.number],
tag: [t.link, t.name, t.tagName, t.angleBracket, t.docString, t.number],
color: 'var(--fg-info)',
},
{ tag: [t.variableName], color: 'var(--fg-success)' },
{ tag: [t.bool], color: 'var(--fg-info)' }, // TODO: Should be pink
{ tag: [t.bool], color: 'var(--fg-warning)' },
{ tag: [t.attributeName, t.propertyName], color: 'var(--fg-primary)' },
{ tag: [t.attributeValue], color: 'var(--fg-warning)' },
{ tag: [t.string], color: 'var(--fg-warning)' }, // TODO: Should be yellow
{ tag: [t.keyword, t.meta, t.operator], color: 'var(--fg-danger)' },
{ tag: [t.string], color: 'var(--fg-notice)' },
{ tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: 'var(--fg-danger)' },
]);
const myTheme = EditorView.theme({}, { dark: true });
// export const defaultHighlightStyle = HighlightStyle.define([
// { tag: t.meta, color: '#404740' },
// { tag: t.link, textDecoration: 'underline' },
// { tag: t.heading, textDecoration: 'underline', fontWeight: 'bold' },
// { tag: t.emphasis, fontStyle: 'italic' },
// { tag: t.strong, fontWeight: 'bold' },
// { tag: t.strikethrough, textDecoration: 'line-through' },
// { tag: t.keyword, color: '#708' },
// { tag: [t.atom, t.bool, t.url, t.contentSeparator, t.labelName], color: '#219' },
// { tag: [t.literal, t.inserted], color: '#164' },
// { tag: [t.string, t.deleted], color: '#a11' },
// { tag: [t.regexp, t.escape, t.special(t.string)], color: '#e40' },
// { tag: t.definition(t.variableName), color: '#00f' },
// { tag: t.local(t.variableName), color: '#30a' },
// { tag: [t.typeName, t.namespace], color: '#085' },
// { tag: t.className, color: '#167' },
// { tag: [t.special(t.variableName), t.macroName], color: '#256' },
// { tag: t.definition(t.propertyName), color: '#00c' },
// { tag: t.comment, color: '#940' },
// { tag: t.invalid, color: '#f00' },
// ]);
const syntaxExtensions: Record<string, LanguageSupport> = {
'application/graphql': graphqlLanguageSupport(),
'application/json': json(),

View File

@@ -66,7 +66,7 @@ const icons = {
export interface IconProps {
icon: keyof typeof icons;
className?: string;
size?: 'xs' | 'sm' | 'md' | 'lg';
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg';
spin?: boolean;
title?: string;
}
@@ -83,6 +83,7 @@ export const Icon = memo(function Icon({ icon, spin, size = 'md', className, tit
size === 'md' && 'h-4 w-4',
size === 'sm' && 'h-3.5 w-3.5',
size === 'xs' && 'h-3 w-3',
size === '2xs' && 'h-2.5 w-2.5',
spin && 'animate-spin',
)}
/>

View File

@@ -57,6 +57,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
size === 'md' && 'w-9',
size === 'sm' && 'w-8',
size === 'xs' && 'w-6',
size === '2xs' && 'w-5',
)}
{...props}
>

View File

@@ -6,8 +6,8 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
<code
className={classNames(
className,
'font-mono text-xs bg-background-highlight-secondary',
'px-1.5 py-0.5 rounded text-fg shadow-inner',
'font-mono text-xs bg-background-highlight-secondary border border-background-highlight',
'px-1.5 py-0.5 rounded text-fg-info shadow-inner',
)}
{...props}
/>

View File

@@ -145,6 +145,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
alignItems="stretch"
className={classNames(
containerClassName,
'x-theme-input',
'relative w-full rounded-md text-fg',
'border',
focused ? 'border-border-focus' : 'border-background-highlight',

View File

@@ -1,3 +1,4 @@
import type { ReactNode } from 'react';
import { useMemo } from 'react';
import type { DropdownItem, DropdownItemSeparator, DropdownProps } from './Dropdown';
import { Dropdown } from './Dropdown';
@@ -9,6 +10,7 @@ export type RadioDropdownItem<T = string | null> =
label: string;
shortLabel?: string;
value: T;
rightSlot?: ReactNode;
}
| DropdownItemSeparator;
@@ -37,9 +39,10 @@ export function RadioDropdown<T = string | null>({
key: item.label,
label: item.label,
shortLabel: item.shortLabel,
rightSlot: item.rightSlot,
onSelect: () => onChange(item.value),
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
};
} as DropdownProps['items'][0];
}
}),
...((extraItems ? [{ type: 'separator' }, ...extraItems] : []) as DropdownItem[]),

View File

@@ -7,12 +7,17 @@ interface Props<T extends string> {
labelClassName?: string;
hideLabel?: boolean;
value: T;
options: { label: string; value: T }[];
options: SelectOption<T>[];
onChange: (value: T) => void;
size?: 'xs' | 'sm' | 'md' | 'lg';
className?: string;
}
export interface SelectOption<T extends string> {
label: string;
value: T;
}
export function Select<T extends string>({
labelPosition = 'top',
name,
@@ -30,6 +35,7 @@ export function Select<T extends string>({
<div
className={classNames(
className,
'x-theme-input',
'w-full',
'pointer-events-auto', // Just in case we're placing in disabled parent
labelPosition === 'left' && 'flex items-center gap-2',

View File

@@ -1,7 +1,6 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { memo, useCallback, useEffect, useRef } from 'react';
import { Button } from '../Button';
import { Icon } from '../Icon';
import type { RadioDropdownProps } from '../RadioDropdown';
import { RadioDropdown } from '../RadioDropdown';
@@ -25,6 +24,7 @@ interface Props {
tabListClassName?: string;
className?: string;
children: ReactNode;
addBorders?: boolean;
}
export function Tabs({
@@ -35,6 +35,7 @@ export function Tabs({
tabs,
className,
tabListClassName,
addBorders,
}: Props) {
const ref = useRef<HTMLDivElement | null>(null);
@@ -78,12 +79,15 @@ export function Tabs({
'-ml-5 pl-3 pr-1 py-1',
)}
>
<HStack space={2} className="flex-shrink-0">
<HStack space={2} className="h-full flex-shrink-0">
{tabs.map((t) => {
const isActive = t.value === value;
const btnClassName = classNames(
isActive ? 'text-fg' : 'text-fg-subtler hover:text-fg-subtle',
'h-full flex items-center text-sm rounded',
'!px-2 ml-[1px]',
addBorders && 'border',
isActive ? 'text-fg' : 'text-fg-subtler hover:text-fg-subtle',
isActive && addBorders ? 'border-background-highlight' : 'border-transparent',
);
if ('options' in t) {
@@ -97,39 +101,34 @@ export function Tabs({
value={t.options.value}
onChange={t.options.onChange}
>
<Button
<button
color="custom"
size="sm"
onClick={isActive ? undefined : () => handleTabChange(t.value)}
className={btnClassName}
rightSlot={
<Icon
size="sm"
icon="chevronDown"
className={classNames(
'-mr-1.5 mt-0.5',
isActive ? 'text-fg-subtle' : 'opacity-50',
)}
/>
}
>
{option && 'shortLabel' in option
? option.shortLabel
: option?.label ?? 'Unknown'}
</Button>
<TabAccent enabled isActive={isActive} />
<Icon
size="sm"
icon="chevronDown"
className={classNames('ml-1', isActive ? 'text-fg-subtle' : 'opacity-50')}
/>
</button>
</RadioDropdown>
);
} else {
return (
<Button
<button
key={t.value}
color="custom"
size="sm"
onClick={() => handleTabChange(t.value)}
className={btnClassName}
>
{t.label}
</Button>
<TabAccent enabled isActive={isActive} />
</button>
);
}
})}
@@ -161,3 +160,14 @@ export const TabContent = memo(function TabContent({
</div>
);
});
function TabAccent({ isActive, enabled }: { isActive: boolean; enabled: boolean }) {
return (
<div
className={classNames(
'w-full opacity-40 border-b-2',
isActive && enabled ? 'border-b-background-highlight' : 'border-b-transparent',
)}
/>
);
}

View File

@@ -52,11 +52,11 @@ export function Toast({
transition={{ duration: 0.2 }}
className={classNames(
className,
'x-theme-dialog',
'x-theme-toast',
'pointer-events-auto',
'relative bg-background pointer-events-auto',
'rounded-lg',
'border border-background-highlight dark:border-background-highlight-secondary shadow-xl',
'border border-background-highlight shadow-lg',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
'w-[22rem] max-h-[80vh]',
'm-2 grid grid-cols-[1fr_auto]',

View File

@@ -0,0 +1,18 @@
import { convertFileSrc } from '@tauri-apps/api/core';
import React from 'react';
import type { HttpResponse } from '../../lib/models';
interface Props {
response: HttpResponse;
}
export function AudioViewer({ response }: Props) {
if (response.bodyPath === null) {
return <div>Empty response body</div>;
}
const src = convertFileSrc(response.bodyPath);
// eslint-disable-next-line jsx-a11y/media-has-caption
return <audio className="w-full" controls src={src}></audio>;
}

View File

@@ -2,7 +2,6 @@ import { convertFileSrc } from '@tauri-apps/api/core';
import classNames from 'classnames';
import { useState } from 'react';
import type { HttpResponse } from '../../lib/models';
import { Button } from '../core/Button';
interface Props {
response: HttpResponse;
@@ -23,13 +22,9 @@ export function ImageViewer({ response, className }: Props) {
<>
<div className="text-sm italic text-fg-subtler">
Response body is too large to preview.{' '}
<Button
className="cursor-pointer underline hover:text-fg"
color="secondary"
onClick={() => setShow(true)}
>
<button className="cursor-pointer underline hover:text-fg" onClick={() => setShow(true)}>
Show anyway
</Button>
</button>
</div>
</>
);

View File

@@ -18,9 +18,10 @@ const extraExtensions = [hyperlink];
interface Props {
response: HttpResponse;
pretty: boolean;
className?: string;
}
export function TextViewer({ response, pretty }: Props) {
export function TextViewer({ response, pretty, className }: Props) {
const [isSearching, toggleIsSearching] = useToggle();
const [filterText, setDebouncedFilterText, setFilterText] = useDebouncedState<string>('', 400);
@@ -85,6 +86,7 @@ export function TextViewer({ response, pretty }: Props) {
return (
<Editor
readOnly
className={className}
forceUpdateKey={body}
defaultValue={body}
contentType={contentType}

View File

@@ -0,0 +1,18 @@
import { convertFileSrc } from '@tauri-apps/api/core';
import React from 'react';
import type { HttpResponse } from '../../lib/models';
interface Props {
response: HttpResponse;
}
export function VideoViewer({ response }: Props) {
if (response.bodyPath === null) {
return <div>Empty response body</div>;
}
const src = convertFileSrc(response.bodyPath);
// eslint-disable-next-line jsx-a11y/media-has-caption
return <video className="w-full" controls src={src}></video>;
}