mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-22 01:19:13 +01:00
Tweak settings for release
This commit is contained in:
@@ -3,7 +3,7 @@ import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { DefaultLayout } from './DefaultLayout';
|
||||
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
|
||||
import RouteError from './RouteError';
|
||||
import { SettingsDialog } from './Settings/SettingsDialog';
|
||||
import { Settings } from './Settings/Settings';
|
||||
import Workspace from './Workspace';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
@@ -41,7 +41,7 @@ const router = createBrowserRouter([
|
||||
path: routePaths.workspaceSettings({
|
||||
workspaceId: ':workspaceId',
|
||||
}),
|
||||
element: <SettingsDialog fullscreen />,
|
||||
element: <Settings />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { settingsQueryKey, useSettings } from '../hooks/useSettings';
|
||||
import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument';
|
||||
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||
import { useZoom } from '../hooks/useZoom';
|
||||
import type { Model } from '../lib/models';
|
||||
@@ -34,7 +33,6 @@ export function GlobalHooks() {
|
||||
|
||||
// Other useful things
|
||||
useSyncThemeToDocument();
|
||||
useSyncWindowTitle();
|
||||
useGlobalCommands();
|
||||
useCommandPalette();
|
||||
useNotificationToast();
|
||||
|
||||
47
src-web/components/Settings/Settings.tsx
Normal file
47
src-web/components/Settings/Settings.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { getCurrent } from '@tauri-apps/api/webviewWindow';
|
||||
import { createGlobalState, useKeyPressEvent } from 'react-use';
|
||||
import { capitalize } from '../../lib/capitalize';
|
||||
import { TabContent, Tabs } from '../core/Tabs/Tabs';
|
||||
import { SettingsAppearance } from './SettingsAppearance';
|
||||
import { SettingsGeneral } from './SettingsGeneral';
|
||||
|
||||
enum Tab {
|
||||
General = 'general',
|
||||
Appearance = 'appearance',
|
||||
}
|
||||
|
||||
const tabs = [Tab.General, Tab.Appearance];
|
||||
const useTabState = createGlobalState<string>(tabs[0]!);
|
||||
|
||||
export const Settings = () => {
|
||||
const [tab, setTab] = useTabState();
|
||||
|
||||
// Close settings window on escape
|
||||
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window
|
||||
useKeyPressEvent('Escape', () => getCurrent().close());
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-[27px] bg-background-highlight-secondary flex items-center justify-center border-b border-background-highlight"
|
||||
>
|
||||
Settings
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
addBorders
|
||||
label="Settings"
|
||||
onChangeValue={setTab}
|
||||
tabs={tabs.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>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -5,19 +5,23 @@ 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 { clamp } from '../../lib/clamp';
|
||||
import { isThemeDark } from '../../lib/theme/window';
|
||||
import type { ButtonProps } from '../core/Button';
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
import { Editor } from '../core/Editor';
|
||||
import type { IconProps } from '../core/Icon';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { PlainInput } from '../core/PlainInput';
|
||||
import type { SelectOption } from '../core/Select';
|
||||
import { Select } from '../core/Select';
|
||||
import { Separator } from '../core/Separator';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
|
||||
const fontSizes = [
|
||||
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||
].map((n) => ({ label: `${n}`, value: `${n}` }));
|
||||
|
||||
const buttonColors: ButtonProps['color'][] = [
|
||||
'primary',
|
||||
'info',
|
||||
@@ -75,39 +79,28 @@ export function SettingsAppearance() {
|
||||
|
||||
return (
|
||||
<VStack space={2} className="mb-4">
|
||||
<PlainInput
|
||||
<Select
|
||||
size="sm"
|
||||
name="interfaceFontSize"
|
||||
label="Font Size"
|
||||
placeholder="16"
|
||||
step={0.5}
|
||||
type="number"
|
||||
labelPosition="left"
|
||||
defaultValue={`${settings.interfaceFontSize}`}
|
||||
validate={(value) => parseInt(value) >= 8 && parseInt(value) <= 30}
|
||||
onChange={(v) =>
|
||||
updateSettings.mutate({
|
||||
...settings,
|
||||
interfaceFontSize: clamp(parseInt(v) || 16, 8, 30),
|
||||
})
|
||||
}
|
||||
value={`${settings.interfaceFontSize}`}
|
||||
options={fontSizes}
|
||||
onChange={(v) => updateSettings.mutate({ interfaceFontSize: parseInt(v) })}
|
||||
/>
|
||||
<PlainInput
|
||||
<Select
|
||||
size="sm"
|
||||
name="editorFontSize"
|
||||
label="Editor Font Size"
|
||||
placeholder="14"
|
||||
step={0.5}
|
||||
type="number"
|
||||
labelPosition="left"
|
||||
defaultValue={`${settings.editorFontSize}`}
|
||||
validate={(value) => parseInt(value) >= 8 && parseInt(value) <= 30}
|
||||
onChange={(v) =>
|
||||
updateSettings.mutate({
|
||||
...settings,
|
||||
editorFontSize: clamp(parseInt(v) || 14, 8, 30),
|
||||
})
|
||||
}
|
||||
value={`${settings.editorFontSize}`}
|
||||
options={fontSizes}
|
||||
onChange={(v) => updateSettings.mutate({ editorFontSize: clamp(parseInt(v) || 14, 8, 30) })}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={settings.editorSoftWrap}
|
||||
title="Wrap Editor Lines"
|
||||
onChange={(editorSoftWrap) => updateSettings.mutate({ editorSoftWrap })}
|
||||
/>
|
||||
|
||||
<Separator className="my-4" />
|
||||
@@ -118,9 +111,8 @@ export function SettingsAppearance() {
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
value={settings.appearance}
|
||||
onChange={async (appearance) => {
|
||||
await updateSettings.mutateAsync({ ...settings, appearance });
|
||||
trackEvent('setting', 'update', { appearance });
|
||||
onChange={(appearance) => {
|
||||
updateSettings.mutateAsync({ appearance });
|
||||
}}
|
||||
options={[
|
||||
{ label: 'Automatic', value: 'system' },
|
||||
@@ -128,42 +120,41 @@ export function SettingsAppearance() {
|
||||
{ label: 'Dark', value: 'dark' },
|
||||
]}
|
||||
/>
|
||||
{(settings.appearance === 'system' || settings.appearance === 'light') && (
|
||||
<Select
|
||||
name="lightTheme"
|
||||
label={settings.appearance === 'system' ? 'Light Theme' : 'Theme'}
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
value={activeTheme.light.id}
|
||||
options={lightThemes}
|
||||
onChange={async (themeLight) => {
|
||||
await updateSettings.mutateAsync({ ...settings, themeLight });
|
||||
trackEvent('setting', 'update', { themeLight });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(settings.appearance === 'system' || settings.appearance === 'dark') && (
|
||||
<Select
|
||||
name="darkTheme"
|
||||
label={settings.appearance === 'system' ? 'Dark Theme' : 'Theme'}
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
value={activeTheme.dark.id}
|
||||
options={darkThemes}
|
||||
onChange={async (themeDark) => {
|
||||
await updateSettings.mutateAsync({ ...settings, themeDark });
|
||||
trackEvent('setting', 'update', { themeDark });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<HStack space={2}>
|
||||
{(settings.appearance === 'system' || settings.appearance === 'light') && (
|
||||
<Select
|
||||
hideLabel
|
||||
leftSlot={<Icon icon="sun" />}
|
||||
name="lightTheme"
|
||||
label="Light Theme"
|
||||
size="sm"
|
||||
value={activeTheme.light.id}
|
||||
options={lightThemes}
|
||||
onChange={(themeLight) => updateSettings.mutateAsync({ ...settings, themeLight })}
|
||||
/>
|
||||
)}
|
||||
{(settings.appearance === 'system' || settings.appearance === 'dark') && (
|
||||
<Select
|
||||
hideLabel
|
||||
name="darkTheme"
|
||||
label="Dark Theme"
|
||||
leftSlot={<Icon icon="moon" />}
|
||||
size="sm"
|
||||
value={activeTheme.dark.id}
|
||||
options={darkThemes}
|
||||
onChange={(themeDark) => updateSettings.mutateAsync({ ...settings, themeDark })}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<VStack
|
||||
space={3}
|
||||
className="mt-3 w-full bg-background p-3 border border-dashed border-background-highlight rounded overflow-x-auto"
|
||||
>
|
||||
<div className="text-fg font-bold">
|
||||
Theme Preview <span className="text-fg-subtle">({appearance})</span>
|
||||
</div>
|
||||
<HStack className="text-fg font-bold" alignItems="center" space={2}>
|
||||
Theme Preview{' '}
|
||||
<Icon icon={appearance === 'dark' ? 'moon' : 'sun'} className="text-fg-subtle" />
|
||||
</HStack>
|
||||
<HStack space={1.5} alignItems="center" className="w-full">
|
||||
{buttonColors.map((c, i) => (
|
||||
<IconButton
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
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>(tabs[0]!);
|
||||
|
||||
interface Props {
|
||||
fullscreen?: true;
|
||||
}
|
||||
|
||||
export const SettingsDialog = ({ fullscreen }: Props) => {
|
||||
const [tab, setTab] = useTabState();
|
||||
const appInfo = useAppInfo();
|
||||
const isDev = appInfo?.isDev ?? false;
|
||||
|
||||
return (
|
||||
<div className={classNames(!fullscreen && 'w-[70vw] max-w-[40rem] h-[80vh]')}>
|
||||
{fullscreen && (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="h-[27px] bg-background-highlight-secondary flex items-center justify-center border-b border-background-highlight"
|
||||
>
|
||||
Settings
|
||||
</div>
|
||||
)}
|
||||
<Tabs
|
||||
value={tab}
|
||||
addBorders
|
||||
label="Settings"
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
@@ -36,10 +35,7 @@ export function SettingsGeneral() {
|
||||
labelPosition="left"
|
||||
size="sm"
|
||||
value={settings.updateChannel}
|
||||
onChange={(updateChannel) => {
|
||||
trackEvent('setting', 'update', { update_channel: updateChannel });
|
||||
updateSettings.mutate({ ...settings, updateChannel });
|
||||
}}
|
||||
onChange={(updateChannel) => updateSettings.mutate({ updateChannel })}
|
||||
options={[
|
||||
{ label: 'Release', value: 'stable' },
|
||||
{ label: 'Early Bird (Beta)', value: 'beta' },
|
||||
@@ -77,23 +73,15 @@ export function SettingsGeneral() {
|
||||
<Checkbox
|
||||
checked={workspace.settingValidateCertificates}
|
||||
title="Validate TLS Certificates"
|
||||
onChange={(settingValidateCertificates) => {
|
||||
trackEvent('workspace', 'update', {
|
||||
validate_certificates: JSON.stringify(settingValidateCertificates),
|
||||
});
|
||||
updateWorkspace.mutate({ settingValidateCertificates });
|
||||
}}
|
||||
onChange={(settingValidateCertificates) =>
|
||||
updateWorkspace.mutate({ settingValidateCertificates })
|
||||
}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={workspace.settingFollowRedirects}
|
||||
title="Follow Redirects"
|
||||
onChange={(settingFollowRedirects) => {
|
||||
trackEvent('workspace', 'update', {
|
||||
follow_redirects: JSON.stringify(settingFollowRedirects),
|
||||
});
|
||||
updateWorkspace.mutate({ settingFollowRedirects });
|
||||
}}
|
||||
onChange={(settingFollowRedirects) => updateWorkspace.mutate({ settingFollowRedirects })}
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
|
||||
@@ -133,27 +133,31 @@ export function Sidebar({ className }: Props) {
|
||||
) {
|
||||
selectedRequest = node.item;
|
||||
}
|
||||
|
||||
const childItems = [...requests, ...folders].filter((f) =>
|
||||
node.item.model === 'workspace' ? f.folderId == null : f.folderId === node.item.id,
|
||||
);
|
||||
|
||||
childItems.sort((a, b) => a.sortPriority - b.sortPriority);
|
||||
// Recurse to children
|
||||
const isCollapsed = collapsed.value?.[node.item.id];
|
||||
const depth = node.depth + 1;
|
||||
childItems.sort((a, b) => a.sortPriority - b.sortPriority);
|
||||
for (const item of childItems) {
|
||||
treeParentMap[item.id] = node;
|
||||
// Add to children
|
||||
node.children.push(next({ item, children: [], depth }));
|
||||
if (item.model !== 'folder') {
|
||||
// Add to selectable requests
|
||||
if (item.model !== 'folder' && !isCollapsed) {
|
||||
selectableRequests.push({ id: item.id, index: selectableRequestIndex++, tree: node });
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
const tree = next({ item: activeWorkspace, children: [], depth: 0 });
|
||||
|
||||
return { tree, treeParentMap, selectableRequests, selectedRequest };
|
||||
}, [activeWorkspace, selectedId, requests, folders]);
|
||||
}, [activeWorkspace, selectedId, requests, folders, collapsed.value]);
|
||||
|
||||
const deleteSelectedRequest = useDeleteRequest(selectedRequest);
|
||||
|
||||
@@ -455,6 +459,7 @@ export function Sidebar({ className }: Props) {
|
||||
/>
|
||||
<SidebarItems
|
||||
treeParentMap={treeParentMap}
|
||||
activeId={activeRequest?.id ?? null}
|
||||
selectedId={selectedId}
|
||||
selectedTree={selectedTree}
|
||||
isCollapsed={isCollapsed}
|
||||
@@ -476,6 +481,7 @@ interface SidebarItemsProps {
|
||||
tree: TreeNode;
|
||||
focused: boolean;
|
||||
draggingId: string | null;
|
||||
activeId: string | null;
|
||||
selectedId: string | null;
|
||||
selectedTree: TreeNode | null;
|
||||
treeParentMap: Record<string, TreeNode>;
|
||||
@@ -491,6 +497,7 @@ interface SidebarItemsProps {
|
||||
function SidebarItems({
|
||||
tree,
|
||||
focused,
|
||||
activeId,
|
||||
selectedId,
|
||||
selectedTree,
|
||||
draggingId,
|
||||
@@ -517,6 +524,7 @@ function SidebarItems({
|
||||
>
|
||||
{tree.children.map((child, i) => {
|
||||
const selected = selectedId === child.item.id;
|
||||
const active = activeId === child.item.id;
|
||||
return (
|
||||
<Fragment key={child.item.id}>
|
||||
{hoveredIndex === i && hoveredTree?.item.id === tree.item.id && <DropMarker />}
|
||||
@@ -535,7 +543,7 @@ function SidebarItems({
|
||||
(child.item.model === 'http_request' || child.item.model === 'grpc_request') && (
|
||||
<HttpMethodTag
|
||||
request={child.item}
|
||||
className={classNames(!selected && 'text-fg-subtler')}
|
||||
className={classNames(!(active || selected) && 'text-fg-subtler')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -558,6 +566,7 @@ function SidebarItems({
|
||||
hoveredTree={hoveredTree}
|
||||
hoveredIndex={hoveredIndex}
|
||||
focused={focused}
|
||||
activeId={activeId}
|
||||
selectedId={selectedId}
|
||||
selectedTree={selectedTree}
|
||||
onSelect={onSelect}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||
import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
@@ -40,6 +41,7 @@ const body = { gridArea: 'body' };
|
||||
const drag = { gridArea: 'drag' };
|
||||
|
||||
export default function Workspace() {
|
||||
useSyncWorkspaceRequestTitle();
|
||||
const workspaces = useWorkspaces();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
|
||||
@@ -373,7 +373,6 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
bottom: upsideDown ? docRect.height - top : undefined,
|
||||
right: onRight ? docRect.width - triggerShape?.right : undefined,
|
||||
left: !onRight ? triggerShape?.left : undefined,
|
||||
width: containerWidth ?? 'auto',
|
||||
};
|
||||
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
|
||||
const triangleStyles = onRight
|
||||
@@ -448,7 +447,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
<HStack
|
||||
space={2}
|
||||
alignItems="center"
|
||||
className="pb-0.5 px-1.5 mb-2 text-sm border border-background-highlight-secondary mx-2 rounded font-mono h-2xs"
|
||||
className="pb-0.5 px-1.5 mb-2 text-sm border border-background-highlight-secondary mx-2 rounded font-mono h-xs"
|
||||
>
|
||||
<Icon icon="search" size="xs" className="text-fg-subtle" />
|
||||
<div className="text-fg">{filter}</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from 'react';
|
||||
import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment';
|
||||
import { useActiveWorkspace } from '../../../hooks/useActiveWorkspace';
|
||||
import { useSettings } from '../../../hooks/useSettings';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { HStack } from '../Stacks';
|
||||
import './Editor.css';
|
||||
@@ -85,11 +86,16 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
}: EditorProps,
|
||||
ref,
|
||||
) {
|
||||
const s = useSettings();
|
||||
const e = useActiveEnvironment();
|
||||
const w = useActiveWorkspace();
|
||||
const environment = autocompleteVariables ? e : null;
|
||||
const workspace = autocompleteVariables ? w : null;
|
||||
|
||||
if (s && wrapLines === undefined) {
|
||||
wrapLines = s.editorSoftWrap;
|
||||
}
|
||||
|
||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||
useImperativeHandle(ref, () => cm.current?.view);
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ const icons = {
|
||||
magicWand: lucide.Wand2Icon,
|
||||
minus: lucide.MinusIcon,
|
||||
moreVertical: lucide.MoreVerticalIcon,
|
||||
moon: lucide.MoonIcon,
|
||||
paste: lucide.ClipboardPasteIcon,
|
||||
pencil: lucide.PencilIcon,
|
||||
pin: lucide.PinIcon,
|
||||
@@ -55,6 +56,7 @@ const icons = {
|
||||
settings2: lucide.Settings2Icon,
|
||||
settings: lucide.SettingsIcon,
|
||||
sparkles: lucide.SparklesIcon,
|
||||
sun: lucide.SunIcon,
|
||||
trash: lucide.TrashIcon,
|
||||
update: lucide.RefreshCcwIcon,
|
||||
upload: lucide.UploadIcon,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props<T extends string> {
|
||||
name: string;
|
||||
@@ -8,6 +10,7 @@ interface Props<T extends string> {
|
||||
labelClassName?: string;
|
||||
hideLabel?: boolean;
|
||||
value: T;
|
||||
leftSlot?: ReactNode;
|
||||
options: SelectOption<T>[];
|
||||
onChange: (value: T) => void;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
@@ -27,10 +30,12 @@ export function Select<T extends string>({
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
leftSlot,
|
||||
onChange,
|
||||
className,
|
||||
size = 'md',
|
||||
}: Props<T>) {
|
||||
const [focused, setFocused] = useState<boolean>(false);
|
||||
const id = `input-${name}`;
|
||||
return (
|
||||
<div
|
||||
@@ -49,25 +54,36 @@ export function Select<T extends string>({
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<select
|
||||
value={value}
|
||||
style={selectBackgroundStyles}
|
||||
onChange={(e) => onChange(e.target.value as T)}
|
||||
<HStack
|
||||
space={2}
|
||||
alignItems="center"
|
||||
className={classNames(
|
||||
'font-mono text-sm border w-full outline-none bg-transparent pl-2 pr-7',
|
||||
'bg-background-highlight-secondary border-background-highlight focus:border-border-focus',
|
||||
'w-full rounded-md text-fg text-sm font-mono',
|
||||
'pl-2',
|
||||
'bg-background-highlight-secondary border',
|
||||
focused ? 'border-border-focus' : 'border-background-highlight',
|
||||
size === 'xs' && 'h-xs',
|
||||
size === 'sm' && 'h-sm',
|
||||
size === 'md' && 'h-md',
|
||||
size === 'lg' && 'h-lg',
|
||||
)}
|
||||
>
|
||||
{options.map(({ label, value }) => (
|
||||
<option key={label} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{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(({ label, value }) => (
|
||||
<option key={label} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user