polish webui and add desktop gateway service support

This commit is contained in:
lpf
2026-03-10 21:25:01 +08:00
parent 74a10ed4e3
commit cfab4cd1cc
22 changed files with 712 additions and 364 deletions

View File

@@ -0,0 +1,208 @@
import React from 'react';
type ButtonVariant = 'neutral' | 'primary' | 'accent' | 'success' | 'warning' | 'danger';
type FixedButtonShape = 'icon' | 'square';
type ButtonSize = 'sm' | 'md' | 'xs' | 'xs_tall' | 'md_tall' | 'md_wide';
type ButtonRadius = 'default' | 'lg' | 'xl' | 'full';
type ButtonGap = 'none' | '1' | '2';
type NativeButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'>;
type NativeAnchorProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'className'>;
type ButtonStyleProps = {
radius?: ButtonRadius;
gap?: ButtonGap;
shadow?: boolean;
noShrink?: boolean;
};
type ButtonProps = NativeButtonProps & ButtonStyleProps & {
variant?: ButtonVariant;
size?: ButtonSize;
fullWidth?: boolean;
grow?: boolean;
};
type LinkButtonProps = NativeAnchorProps & ButtonStyleProps & {
variant?: ButtonVariant;
size?: ButtonSize;
fullWidth?: boolean;
grow?: boolean;
};
type FixedButtonProps = NativeButtonProps & ButtonStyleProps & {
label: string;
shape?: FixedButtonShape;
variant?: ButtonVariant;
};
type FixedLinkButtonProps = NativeAnchorProps & ButtonStyleProps & {
label: string;
shape?: FixedButtonShape;
variant?: ButtonVariant;
};
function joinClasses(...values: Array<string | undefined | false>) {
return values.filter(Boolean).join(' ');
}
function buttonSizeClass(size: ButtonSize) {
switch (size) {
case 'xs':
return 'px-2.5 py-1 text-xs';
case 'xs_tall':
return 'px-2.5 py-2 text-xs';
case 'sm':
return 'px-3 py-1.5 text-sm';
case 'md_tall':
return 'px-4 py-2.5 text-sm font-medium';
case 'md_wide':
return 'px-6 py-2 text-sm font-medium';
case 'md':
default:
return 'px-4 py-2 text-sm font-medium';
}
}
function buttonRadiusClass(radius: ButtonRadius) {
switch (radius) {
case 'lg':
return 'rounded-lg';
case 'xl':
return 'rounded-xl';
case 'full':
return 'rounded-full';
case 'default':
default:
return undefined;
}
}
function buttonGapClass(gap: ButtonGap) {
switch (gap) {
case '1':
return 'gap-1';
case '2':
return 'gap-2';
case 'none':
default:
return undefined;
}
}
function buttonClass(
variant: ButtonVariant,
size: ButtonSize,
fullWidth: boolean,
grow: boolean,
radius: ButtonRadius,
gap: ButtonGap,
shadow: boolean,
noShrink: boolean,
) {
return joinClasses(
'ui-button',
`ui-button-${variant}`,
buttonSizeClass(size),
fullWidth && 'w-full',
grow && 'flex-1',
buttonRadiusClass(radius),
buttonGapClass(gap),
shadow && 'shadow-sm',
noShrink && 'shrink-0',
);
}
export function Button({
variant = 'neutral',
size = 'md',
fullWidth = false,
grow = false,
radius = 'default',
gap = 'none',
shadow = false,
noShrink = false,
type = 'button',
...props
}: ButtonProps) {
return <button {...props} type={type} className={buttonClass(variant, size, fullWidth, grow, radius, gap, shadow, noShrink)} />;
}
export function LinkButton({
variant = 'neutral',
size = 'md',
fullWidth = false,
grow = false,
radius = 'default',
gap = 'none',
shadow = false,
noShrink = false,
...props
}: LinkButtonProps) {
return <a {...props} className={buttonClass(variant, size, fullWidth, grow, radius, gap, shadow, noShrink)} />;
}
export function FixedButton({
label,
shape = 'icon',
variant = 'neutral',
radius = 'default',
gap = 'none',
shadow = false,
noShrink = false,
type = 'button',
title,
children,
...props
}: FixedButtonProps) {
return (
<button
{...props}
type={type}
title={title || label}
aria-label={props['aria-label'] || label}
className={joinClasses(
'ui-button',
`ui-button-${variant}`,
shape === 'square' ? 'ui-button-square' : 'ui-button-icon',
buttonRadiusClass(radius),
buttonGapClass(gap),
shadow && 'shadow-sm',
noShrink && 'shrink-0',
)}
>
{children}
</button>
);
}
export function FixedLinkButton({
label,
shape = 'icon',
variant = 'neutral',
radius = 'default',
gap = 'none',
shadow = false,
noShrink = false,
title,
children,
...props
}: FixedLinkButtonProps) {
return (
<a
{...props}
title={title || label}
aria-label={props['aria-label'] || label}
className={joinClasses(
'ui-button',
`ui-button-${variant}`,
shape === 'square' ? 'ui-button-square' : 'ui-button-icon',
buttonRadiusClass(radius),
buttonGapClass(gap),
shadow && 'shadow-sm',
noShrink && 'shrink-0',
)}
>
{children}
</a>
);
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { AnimatePresence, motion } from 'motion/react';
import { useTranslation } from 'react-i18next';
import { Button } from './Button';
type DialogOptions = {
title?: string;
@@ -61,11 +62,11 @@ export const GlobalDialog: React.FC<{
</div>
<div className="px-5 pb-5 flex items-center justify-end gap-2 relative z-[1]">
{(kind === 'confirm' || kind === 'prompt') && (
<button onClick={onCancel} className="ui-button ui-button-neutral px-3 py-1.5 text-sm">{options.cancelText || t('cancel')}</button>
<Button onClick={onCancel} size="sm">{options.cancelText || t('cancel')}</Button>
)}
<button onClick={() => onConfirm(kind === 'prompt' ? value : undefined)} className={`ui-button px-3 py-1.5 text-sm ${options.danger ? 'ui-button-danger' : 'ui-button-primary'}`}>
<Button onClick={() => onConfirm(kind === 'prompt' ? value : undefined)} variant={options.danger ? 'danger' : 'primary'} size="sm">
{options.confirmText || t('dialogOk')}
</button>
</Button>
</div>
</motion.div>
</motion.div>

View File

@@ -3,6 +3,7 @@ import { Github, Moon, RefreshCw, SunMedium, Terminal } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { FixedButton, FixedLinkButton } from './Button';
const REPO_URL = 'https://github.com/YspCoder/clawgo';
@@ -88,41 +89,21 @@ const Header: React.FC = () => {
<div className="ui-border-subtle hidden md:block h-5 w-px bg-transparent border-l" />
<a
href={REPO_URL}
target="_blank"
rel="noreferrer"
className="ui-button ui-button-neutral ui-button-icon text-sm font-medium"
title={t('githubRepo')}
>
<FixedLinkButton href={REPO_URL} target="_blank" rel="noreferrer" label={t('githubRepo')}>
<Github className="w-4 h-4" />
</a>
</FixedLinkButton>
<button
onClick={checkVersion}
disabled={checkingVersion}
className="ui-button ui-button-neutral ui-button-icon text-sm font-medium disabled:opacity-60"
title={t('checkVersion')}
>
<FixedButton onClick={checkVersion} disabled={checkingVersion} label={t('checkVersion')}>
<RefreshCw className={`w-4 h-4 ${checkingVersion ? 'animate-spin' : ''}`} />
</button>
</FixedButton>
<button
onClick={toggleTheme}
className="ui-button ui-button-neutral ui-button-icon text-sm font-medium"
title={theme === 'dark' ? t('themeLight') : t('themeDark')}
>
<FixedButton onClick={toggleTheme} label={theme === 'dark' ? t('themeLight') : t('themeDark')}>
{theme === 'dark' ? <SunMedium className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
</button>
</FixedButton>
<button
onClick={toggleLang}
className="ui-button ui-button-neutral ui-button-square text-sm font-semibold"
title={i18n.language === 'en' ? t('languageZh') : t('languageEn')}
aria-label={i18n.language === 'en' ? t('languageZh') : t('languageEn')}
>
<FixedButton onClick={toggleLang} shape="square" label={i18n.language === 'en' ? t('languageZh') : t('languageEn')}>
{i18n.language === 'en' ? '中' : 'EN'}
</button>
</FixedButton>
</div>
</header>
);

View File

@@ -1,6 +1,7 @@
import React, { useMemo, useState } from 'react';
import { Plus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { FixedButton } from './Button';
interface RecursiveConfigProps {
data: any;
@@ -79,17 +80,16 @@ const PrimitiveArrayEditor: React.FC<{
))}
</datalist>
<button
<FixedButton
onClick={() => {
addValue(draft);
setDraft('');
}}
className="ui-button ui-button-neutral ui-button-icon rounded-xl"
title={t('add')}
aria-label={t('add')}
radius="xl"
label={t('add')}
>
<Plus className="w-4 h-4" />
</button>
</FixedButton>
<select
value={selected}