Initial settings implementation

This commit is contained in:
Gregory Schier
2024-01-11 21:13:17 -08:00
parent 11161fda51
commit 202e272e90
18 changed files with 426 additions and 65 deletions

View File

@@ -0,0 +1,33 @@
import { useSettings } from '../hooks/useSettings';
import { useTheme } from '../hooks/useTheme';
import { useUpdateSettings } from '../hooks/useUpdateSettings';
import { Checkbox } from './core/Checkbox';
import { VStack } from './core/Stacks';
export const SettingsDialog = () => {
const { appearance, toggleAppearance } = useTheme();
const settings = useSettings();
const updateSettings = useUpdateSettings();
if (settings == null) {
return null;
}
return (
<VStack space={2}>
<Checkbox
checked={settings.validateCertificates}
title="Validate TLS Certificates"
onChange={(validateCertificates) =>
updateSettings.mutateAsync({ ...settings, validateCertificates })
}
/>
<Checkbox
checked={settings.followRedirects}
title="Follow Redirects"
onChange={(followRedirects) => updateSettings.mutateAsync({ ...settings, followRedirects })}
/>
<Checkbox checked={appearance === 'dark'} title="Dark Mode" onChange={toggleAppearance} />
</VStack>
);
};

View File

@@ -13,6 +13,7 @@ import { IconButton } from './core/IconButton';
import { VStack } from './core/Stacks';
import { useDialog } from './DialogContext';
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
import { SettingsDialog } from './SettingsDialog';
export function SettingsDropdown() {
const importData = useImportData();
@@ -61,16 +62,11 @@ export function SettingsDropdown() {
leftSlot: <Icon icon="upload" />,
onSelect: () => exportData.mutate(),
},
{
key: 'appearance',
label: 'Toggle Theme',
onSelect: toggleAppearance,
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
},
{
key: 'hotkeys',
label: 'Keyboard shortcuts',
hotkeyAction: 'hotkeys.showHelp',
leftSlot: <Icon icon="keyboard" />,
onSelect: () => {
dialog.show({
id: 'hotkey-help',
@@ -79,7 +75,20 @@ export function SettingsDropdown() {
render: () => <KeyboardShortcutsDialog />,
});
},
leftSlot: <Icon icon="keyboard" />,
},
{
key: 'settings',
label: 'Settings',
hotkeyAction: 'settings.show',
leftSlot: <Icon icon="gear" />,
onSelect: () => {
dialog.show({
id: 'settings',
size: 'md',
title: 'Settings',
render: () => <SettingsDialog />,
});
},
},
{ type: 'separator', label: `Yaak v${appVersion.data}` },
{

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { useCallback } from 'react';
import { Icon } from './Icon';
import { HStack } from './Stacks';
interface Props {
checked: boolean;
@@ -8,33 +8,47 @@ interface Props {
onChange: (checked: boolean) => void;
disabled?: boolean;
className?: string;
hideLabel?: boolean;
}
export function Checkbox({ checked, onChange, className, disabled, title }: Props) {
const handleClick = useCallback(() => {
onChange(!checked);
}, [onChange, checked]);
export function Checkbox({ checked, onChange, className, disabled, title, hideLabel }: Props) {
return (
<button
role="checkbox"
aria-checked={checked ? 'true' : 'false'}
disabled={disabled}
onClick={handleClick}
title={title}
className={classNames(
className,
'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',
'focus:border-focus',
'disabled:opacity-disabled',
checked && 'bg-gray-200/10',
// Remove focus style
'outline-none',
)}
<HStack
as="label"
space={2}
alignItems="center"
className={classNames(className, disabled && 'opacity-disabled')}
>
<div className="flex items-center justify-center">
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
<div className="relative flex">
<input
aria-hidden
className="appearance-none w-4 h-4 flex-shrink-0 border border-gray-200 rounded focus:border-focus outline-none ring-0"
type="checkbox"
disabled={disabled}
onChange={() => onChange(!checked)}
/>
<div className="absolute inset-0 flex items-center justify-center">
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
</div>
</div>
</button>
{/*<button*/}
{/* role="checkbox"*/}
{/* aria-checked={checked ? 'true' : 'false'}*/}
{/* disabled={disabled}*/}
{/* onClick={handleClick}*/}
{/* title={title}*/}
{/* className={classNames(*/}
{/* className,*/}
{/* 'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',*/}
{/* 'focus:border-focus',*/}
{/* 'disabled:opacity-disabled',*/}
{/* checked && 'bg-gray-200/10',*/}
{/* // Remove focus style*/}
{/* 'outline-none',*/}
{/* )}*/}
{/*>*/}
{/*</button>*/}
{!hideLabel && title}
</HStack>
);
}

View File

@@ -479,11 +479,6 @@ interface MenuItemHotKeyProps {
}
function MenuItemHotKey({ action, onSelect, item }: MenuItemHotKeyProps) {
if (action) {
console.log('MENU ITEM HOTKEY', action, item);
}
useHotKey(action ?? null, () => {
onSelect(item);
});
useHotKey(action ?? null, () => onSelect(item));
return null;
}

View File

@@ -290,13 +290,29 @@ function getExtensions({
// Handle onChange
EditorView.updateListener.of((update) => {
if (onChange && update.docChanged) {
// Only fire onChange if the document changed and the update was from user input. This prevents firing onChange when the document is updated when
// changing pages (one request to another in header editor)
if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) {
onChange.current?.(update.state.doc.toString());
}
}),
];
}
function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) {
// Make sure document has changed, ensuring user events like selections don't count.
if (viewUpdate.docChanged) {
// Check transactions for any that are direct user input, not changes from Y.js or another extension.
for (const transaction of viewUpdate.transactions) {
// Not using Transaction.isUserEvent because that only checks for a specific User event type ( "input", "delete", etc.). Checking the annotation directly allows for any type of user event.
const userEventType = transaction.annotation(Transaction.userEvent);
if (userEventType) return userEventType;
}
}
return false;
}
const syncGutterBg = ({
parent,
className = '',

View File

@@ -335,7 +335,8 @@ const FormRow = memo(function FormRow({
<span className="w-3" />
)}
<Checkbox
title={pairContainer.pair.enabled ? 'disable entry' : 'Enable item'}
hideLabel
title={pairContainer.pair.enabled ? 'Disable item' : 'Enable item'}
disabled={isLast}
checked={isLast ? false : !!pairContainer.pair.enabled}
className={classNames('mr-2', isLast && '!opacity-disabled')}

View File

@@ -54,7 +54,7 @@ export const VStack = forwardRef(function VStack(
});
type BaseStackProps = HTMLAttributes<HTMLElement> & {
as?: ComponentType | 'ul' | 'form';
as?: ComponentType | 'ul' | 'label' | 'form';
space?: keyof typeof gapClasses;
alignItems?: 'start' | 'center' | 'stretch';
justifyContent?: 'start' | 'center' | 'end' | 'between';