mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-27 03:41:11 +01:00
A bunch more theme stuff
This commit is contained in:
162
src-web/components/Settings/SettingsAppearance.tsx
Normal file
162
src-web/components/Settings/SettingsAppearance.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
103
src-web/components/Settings/SettingsDesign.tsx
Normal file
103
src-web/components/Settings/SettingsDesign.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
51
src-web/components/Settings/SettingsDialog.tsx
Normal file
51
src-web/components/Settings/SettingsDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
116
src-web/components/Settings/SettingsGeneral.tsx
Normal file
116
src-web/components/Settings/SettingsGeneral.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
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 function SettingsGeneral() {
|
||||
const workspace = useActiveWorkspace();
|
||||
const updateWorkspace = useUpdateWorkspace(workspace?.id ?? null);
|
||||
const settings = useSettings();
|
||||
const updateSettings = useUpdateSettings();
|
||||
const appInfo = useAppInfo();
|
||||
const checkForUpdates = useCheckForUpdates();
|
||||
|
||||
if (settings == null || workspace == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack space={2} className="mb-4">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1">
|
||||
<Select
|
||||
name="updateChannel"
|
||||
label="Update Channel"
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
value={settings.updateChannel}
|
||||
onChange={async (updateChannel) => {
|
||||
trackEvent('setting', 'update', { update_channel: updateChannel });
|
||||
await updateSettings.mutateAsync({ ...settings, updateChannel });
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: 'Release',
|
||||
value: 'stable',
|
||||
},
|
||||
{
|
||||
label: 'Early Bird (Beta)',
|
||||
value: 'beta',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<IconButton
|
||||
variant="border"
|
||||
size="sm"
|
||||
title="Check for updates"
|
||||
icon="refresh"
|
||||
spin={checkForUpdates.isPending}
|
||||
onClick={() => checkForUpdates.mutateAsync()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<Heading size={2}>
|
||||
Workspace{' '}
|
||||
<div className="inline-block ml-1 bg-background-highlight px-2 py-0.5 text-sm rounded text-fg">
|
||||
{workspace.name}
|
||||
</div>
|
||||
</Heading>
|
||||
<VStack className="mt-1 w-full" space={3}>
|
||||
<Input
|
||||
size="sm"
|
||||
name="requestTimeout"
|
||||
label="Request Timeout (ms)"
|
||||
placeholder="0"
|
||||
labelPosition="left"
|
||||
defaultValue={`${workspace.settingRequestTimeout}`}
|
||||
validate={(value) => parseInt(value) >= 0}
|
||||
onChange={(v) => updateWorkspace.mutateAsync({ settingRequestTimeout: parseInt(v) || 0 })}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={workspace.settingValidateCertificates}
|
||||
title="Validate TLS Certificates"
|
||||
onChange={async (settingValidateCertificates) => {
|
||||
trackEvent('workspace', 'update', {
|
||||
validate_certificates: JSON.stringify(settingValidateCertificates),
|
||||
});
|
||||
await updateWorkspace.mutateAsync({ settingValidateCertificates });
|
||||
}}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={workspace.settingFollowRedirects}
|
||||
title="Follow Redirects"
|
||||
onChange={async (settingFollowRedirects) => {
|
||||
trackEvent('workspace', 'update', {
|
||||
follow_redirects: JSON.stringify(settingFollowRedirects),
|
||||
});
|
||||
await updateWorkspace.mutateAsync({ settingFollowRedirects });
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<Heading size={2}>App Info</Heading>
|
||||
<KeyValueRows>
|
||||
<KeyValueRow label="Version" value={appInfo?.version} />
|
||||
<KeyValueRow label="Data Directory" value={appInfo?.appDataDir} />
|
||||
<KeyValueRow label="Logs Directory" value={appInfo?.appLogDir} />
|
||||
</KeyValueRows>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user