mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-29 05:31:51 +02:00
<Select> uses custom component on Windows
This commit is contained in:
@@ -14,7 +14,7 @@ import { Editor } from '../core/Editor';
|
|||||||
import type { IconProps } from '../core/Icon';
|
import type { IconProps } from '../core/Icon';
|
||||||
import { Icon } from '../core/Icon';
|
import { Icon } from '../core/Icon';
|
||||||
import { IconButton } from '../core/IconButton';
|
import { IconButton } from '../core/IconButton';
|
||||||
import type { SelectOption } from '../core/Select';
|
import type { SelectProps } from '../core/Select';
|
||||||
import { Select } from '../core/Select';
|
import { Select } from '../core/Select';
|
||||||
import { Separator } from '../core/Separator';
|
import { Separator } from '../core/Separator';
|
||||||
import { HStack, VStack } from '../core/Stacks';
|
import { HStack, VStack } from '../core/Stacks';
|
||||||
@@ -64,14 +64,14 @@ export function SettingsAppearance() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lightThemes: SelectOption<string>[] = themes
|
const lightThemes: SelectProps<string>['options'] = themes
|
||||||
.filter((theme) => !isThemeDark(theme))
|
.filter((theme) => !isThemeDark(theme))
|
||||||
.map((theme) => ({
|
.map((theme) => ({
|
||||||
label: theme.name,
|
label: theme.name,
|
||||||
value: theme.id,
|
value: theme.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const darkThemes: SelectOption<string>[] = themes
|
const darkThemes: SelectProps<string>['options'] = themes
|
||||||
.filter((theme) => isThemeDark(theme))
|
.filter((theme) => isThemeDark(theme))
|
||||||
.map((theme) => ({
|
.map((theme) => ({
|
||||||
label: theme.name,
|
label: theme.name,
|
||||||
@@ -131,8 +131,9 @@ export function SettingsAppearance() {
|
|||||||
name="lightTheme"
|
name="lightTheme"
|
||||||
label="Light Theme"
|
label="Light Theme"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
value={activeTheme.light.id}
|
value={activeTheme.light.id}
|
||||||
options={[{ label: 'Light Mode Themes', options: lightThemes }]}
|
options={lightThemes}
|
||||||
onChange={(themeLight) => {
|
onChange={(themeLight) => {
|
||||||
trackEvent('theme', 'update', { theme: themeLight, appearance: 'light' });
|
trackEvent('theme', 'update', { theme: themeLight, appearance: 'light' });
|
||||||
updateSettings.mutateAsync({ ...settings, themeLight });
|
updateSettings.mutateAsync({ ...settings, themeLight });
|
||||||
@@ -143,11 +144,12 @@ export function SettingsAppearance() {
|
|||||||
<Select
|
<Select
|
||||||
hideLabel
|
hideLabel
|
||||||
name="darkTheme"
|
name="darkTheme"
|
||||||
|
className="flex-1"
|
||||||
label="Dark Theme"
|
label="Dark Theme"
|
||||||
leftSlot={<Icon icon="moon" />}
|
leftSlot={<Icon icon="moon" />}
|
||||||
size="sm"
|
size="sm"
|
||||||
value={activeTheme.dark.id}
|
value={activeTheme.dark.id}
|
||||||
options={[{ label: 'Dark Mode Themes', options: darkThemes }]}
|
options={darkThemes}
|
||||||
onChange={(themeDark) => {
|
onChange={(themeDark) => {
|
||||||
trackEvent('theme', 'update', { theme: themeDark, appearance: 'dark' });
|
trackEvent('theme', 'update', { theme: themeDark, appearance: 'dark' });
|
||||||
updateSettings.mutateAsync({ ...settings, themeDark });
|
updateSettings.mutateAsync({ ...settings, themeDark });
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { useUpdateWorkspace } from '../../hooks/useUpdateWorkspace';
|
|||||||
import { Checkbox } from '../core/Checkbox';
|
import { Checkbox } from '../core/Checkbox';
|
||||||
import { Heading } from '../core/Heading';
|
import { Heading } from '../core/Heading';
|
||||||
import { IconButton } from '../core/IconButton';
|
import { IconButton } from '../core/IconButton';
|
||||||
import { Input } from '../core/Input';
|
|
||||||
import { KeyValueRow, KeyValueRows } from '../core/KeyValueRow';
|
import { KeyValueRow, KeyValueRows } from '../core/KeyValueRow';
|
||||||
|
import { PlainInput } from '../core/PlainInput';
|
||||||
import { Select } from '../core/Select';
|
import { Select } from '../core/Select';
|
||||||
import { Separator } from '../core/Separator';
|
import { Separator } from '../core/Separator';
|
||||||
import { VStack } from '../core/Stacks';
|
import { VStack } from '../core/Stacks';
|
||||||
@@ -59,7 +59,7 @@ export function SettingsGeneral() {
|
|||||||
</div>
|
</div>
|
||||||
</Heading>
|
</Heading>
|
||||||
<VStack className="mt-1 w-full" space={3}>
|
<VStack className="mt-1 w-full" space={3}>
|
||||||
<Input
|
<PlainInput
|
||||||
size="sm"
|
size="sm"
|
||||||
name="requestTimeout"
|
name="requestTimeout"
|
||||||
label="Request Timeout (ms)"
|
label="Request Timeout (ms)"
|
||||||
@@ -68,6 +68,7 @@ export function SettingsGeneral() {
|
|||||||
defaultValue={`${workspace.settingRequestTimeout}`}
|
defaultValue={`${workspace.settingRequestTimeout}`}
|
||||||
validate={(value) => parseInt(value) >= 0}
|
validate={(value) => parseInt(value) >= 0}
|
||||||
onChange={(v) => updateWorkspace.mutate({ settingRequestTimeout: parseInt(v) || 0 })}
|
onChange={(v) => updateWorkspace.mutate({ settingRequestTimeout: parseInt(v) || 0 })}
|
||||||
|
type="number"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Icon icon="refresh" size={size} className="animate-spin mr-1" />
|
<Icon icon="refresh" size={size} className="animate-spin mr-1" />
|
||||||
) : leftSlot ? (
|
) : leftSlot ? (
|
||||||
<div className="mr-1">{leftSlot}</div>
|
<div className="mr-2">{leftSlot}</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface DropdownProps {
|
|||||||
items: DropdownItem[];
|
items: DropdownItem[];
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
fullWidth?: boolean;
|
||||||
hotKeyAction?: HotkeyAction;
|
hotKeyAction?: HotkeyAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ export interface DropdownRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
||||||
{ children, items, onOpen, onClose, hotKeyAction }: DropdownProps,
|
{ children, items, onOpen, onClose, hotKeyAction, fullWidth }: DropdownProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const [isOpen, _setIsOpen] = useState<boolean>(false);
|
const [isOpen, _setIsOpen] = useState<boolean>(false);
|
||||||
@@ -153,6 +154,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
|||||||
<Menu
|
<Menu
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
showTriangle
|
showTriangle
|
||||||
|
fullWidth={fullWidth}
|
||||||
defaultSelectedIndex={defaultSelectedIndex}
|
defaultSelectedIndex={defaultSelectedIndex}
|
||||||
items={items}
|
items={items}
|
||||||
triggerShape={triggerRect ?? null}
|
triggerShape={triggerRect ?? null}
|
||||||
@@ -203,6 +205,7 @@ interface MenuProps {
|
|||||||
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'> | null;
|
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'> | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
showTriangle?: boolean;
|
showTriangle?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,6 +214,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
|||||||
className,
|
className,
|
||||||
isOpen,
|
isOpen,
|
||||||
items,
|
items,
|
||||||
|
fullWidth,
|
||||||
onClose,
|
onClose,
|
||||||
triggerShape,
|
triggerShape,
|
||||||
defaultSelectedIndex,
|
defaultSelectedIndex,
|
||||||
@@ -359,21 +363,23 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
|||||||
const heightAbove = triggerShape.top;
|
const heightAbove = triggerShape.top;
|
||||||
const heightBelow = docRect.height - triggerShape.bottom;
|
const heightBelow = docRect.height - triggerShape.bottom;
|
||||||
const hSpaceRemaining = docRect.width - triggerShape.left;
|
const hSpaceRemaining = docRect.width - triggerShape.left;
|
||||||
const top = triggerShape?.bottom + 5;
|
const top = triggerShape.bottom + 5;
|
||||||
const onRight = hSpaceRemaining < 200;
|
const onRight = hSpaceRemaining < 200;
|
||||||
const upsideDown = heightAbove > heightBelow && heightBelow < 200;
|
const upsideDown = heightAbove > heightBelow && heightBelow < 200;
|
||||||
|
const triggerWidth = triggerShape.right - triggerShape.left;
|
||||||
const containerStyles = {
|
const containerStyles = {
|
||||||
top: !upsideDown ? top : undefined,
|
top: !upsideDown ? top : undefined,
|
||||||
bottom: upsideDown ? docRect.height - top : undefined,
|
bottom: upsideDown ? docRect.height - top : undefined,
|
||||||
right: onRight ? docRect.width - triggerShape?.right : undefined,
|
right: onRight ? docRect.width - triggerShape.right : undefined,
|
||||||
left: !onRight ? triggerShape?.left : undefined,
|
left: !onRight ? triggerShape.left : undefined,
|
||||||
|
minWidth: fullWidth ? triggerWidth : undefined,
|
||||||
};
|
};
|
||||||
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
|
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
|
||||||
const triangleStyles = onRight
|
const triangleStyles = onRight
|
||||||
? { right: width / 2, marginRight: '-0.2rem', ...size }
|
? { right: width / 2, marginRight: '-0.2rem', ...size }
|
||||||
: { left: width / 2, marginLeft: '-0.2rem', ...size };
|
: { left: width / 2, marginLeft: '-0.2rem', ...size };
|
||||||
return { containerStyles, triangleStyles };
|
return { containerStyles, triangleStyles };
|
||||||
}, [triggerShape]);
|
}, [fullWidth, triggerShape]);
|
||||||
|
|
||||||
const filteredItems = useMemo(
|
const filteredItems = useMemo(
|
||||||
() => items.filter((i) => getNodeText(i.label).toLowerCase().includes(filter.toLowerCase())),
|
() => items.filter((i) => getNodeText(i.label).toLowerCase().includes(filter.toLowerCase())),
|
||||||
|
|||||||
@@ -50,5 +50,9 @@ export function RadioDropdown<T = string | null>({
|
|||||||
[items, extraItems, value, onChange],
|
[items, extraItems, value, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Dropdown items={dropdownItems}>{children}</Dropdown>;
|
return (
|
||||||
|
<Dropdown fullWidth items={dropdownItems}>
|
||||||
|
{children}
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { CSSProperties, ReactNode } from 'react';
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useOsInfo } from '../../hooks/useOsInfo';
|
||||||
|
import type { ButtonProps } from './Button';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import type { RadioDropdownItem } from './RadioDropdown';
|
||||||
|
import { RadioDropdown } from './RadioDropdown';
|
||||||
import { HStack } from './Stacks';
|
import { HStack } from './Stacks';
|
||||||
|
|
||||||
interface Props<T extends string> {
|
export interface SelectProps<T extends string> {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
labelPosition?: 'top' | 'left';
|
labelPosition?: 'top' | 'left';
|
||||||
@@ -11,22 +16,12 @@ interface Props<T extends string> {
|
|||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
value: T;
|
value: T;
|
||||||
leftSlot?: ReactNode;
|
leftSlot?: ReactNode;
|
||||||
options: SelectOption<T>[] | SelectOptionGroup<T>[];
|
options: RadioDropdownItem<T>[];
|
||||||
onChange: (value: T) => void;
|
onChange: (value: T) => void;
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
size?: ButtonProps['size'];
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectOption<T extends string> {
|
|
||||||
label: string;
|
|
||||||
value: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelectOptionGroup<T extends string> {
|
|
||||||
label: string;
|
|
||||||
options: SelectOption<T>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Select<T extends string>({
|
export function Select<T extends string>({
|
||||||
labelPosition = 'top',
|
labelPosition = 'top',
|
||||||
name,
|
name,
|
||||||
@@ -39,7 +34,8 @@ export function Select<T extends string>({
|
|||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
}: Props<T>) {
|
}: SelectProps<T>) {
|
||||||
|
const osInfo = useOsInfo();
|
||||||
const [focused, setFocused] = useState<boolean>(false);
|
const [focused, setFocused] = useState<boolean>(false);
|
||||||
const id = `input-${name}`;
|
const id = `input-${name}`;
|
||||||
return (
|
return (
|
||||||
@@ -49,7 +45,7 @@ export function Select<T extends string>({
|
|||||||
'x-theme-input',
|
'x-theme-input',
|
||||||
'w-full',
|
'w-full',
|
||||||
'pointer-events-auto', // Just in case we're placing in disabled parent
|
'pointer-events-auto', // Just in case we're placing in disabled parent
|
||||||
labelPosition === 'left' && 'flex items-center gap-2',
|
labelPosition === 'left' && 'grid grid-cols-[auto_1fr] items-center gap-2',
|
||||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -59,45 +55,54 @@ export function Select<T extends string>({
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<HStack
|
{osInfo?.osType === 'macos' ? (
|
||||||
space={2}
|
<HStack
|
||||||
className={classNames(
|
space={2}
|
||||||
'w-full rounded-md text-fg text-sm font-mono',
|
className={classNames(
|
||||||
'pl-2',
|
'w-full rounded-md text-fg text-sm font-mono',
|
||||||
'border',
|
'pl-2',
|
||||||
focused ? 'border-border-focus' : 'border-background-highlight',
|
'border',
|
||||||
size === 'xs' && 'h-xs',
|
focused ? 'border-border-focus' : 'border-background-highlight',
|
||||||
size === 'sm' && 'h-sm',
|
size === 'xs' && 'h-xs',
|
||||||
size === 'md' && 'h-md',
|
size === 'sm' && 'h-sm',
|
||||||
size === 'lg' && 'h-lg',
|
size === 'md' && 'h-md',
|
||||||
)}
|
|
||||||
>
|
|
||||||
{leftSlot && <div>{leftSlot}</div>}
|
|
||||||
<select
|
|
||||||
value={value}
|
|
||||||
style={selectBackgroundStyles}
|
|
||||||
onChange={(e) => onChange(e.target.value as T)}
|
|
||||||
onFocus={() => setFocused(true)}
|
|
||||||
onBlur={() => setFocused(false)}
|
|
||||||
className={classNames('pr-7 w-full outline-none bg-transparent')}
|
|
||||||
>
|
|
||||||
{options.map((o) =>
|
|
||||||
'options' in o ? (
|
|
||||||
<optgroup key={o.label} label={o.label}>
|
|
||||||
{o.options.map(({ label, value }) => (
|
|
||||||
<option key={label} value={value}>
|
|
||||||
{label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</optgroup>
|
|
||||||
) : (
|
|
||||||
<option key={o.label} value={o.value}>
|
|
||||||
{o.label}
|
|
||||||
</option>
|
|
||||||
),
|
|
||||||
)}
|
)}
|
||||||
</select>
|
>
|
||||||
</HStack>
|
{leftSlot && <div>{leftSlot}</div>}
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
style={selectBackgroundStyles}
|
||||||
|
onChange={(e) => onChange(e.target.value as T)}
|
||||||
|
onFocus={() => setFocused(true)}
|
||||||
|
onBlur={() => setFocused(false)}
|
||||||
|
className={classNames('pr-7 w-full outline-none bg-transparent')}
|
||||||
|
>
|
||||||
|
{options.map((o) => {
|
||||||
|
if (o.type === 'separator') return null;
|
||||||
|
return (
|
||||||
|
<option key={o.label} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</HStack>
|
||||||
|
) : (
|
||||||
|
// Use custom "select" component until Tauri can be configured to have select menus not always appear in
|
||||||
|
// light mode
|
||||||
|
<RadioDropdown value={value} onChange={onChange} items={options}>
|
||||||
|
<Button
|
||||||
|
className="w-full text-sm font-mono"
|
||||||
|
justify="start"
|
||||||
|
variant="border"
|
||||||
|
size={size}
|
||||||
|
leftSlot={leftSlot}
|
||||||
|
forDropdown
|
||||||
|
>
|
||||||
|
{options.find((o) => o.type !== 'separator' && o.value === value)?.label ?? '--'}
|
||||||
|
</Button>
|
||||||
|
</RadioDropdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user