mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-28 01:17:28 +08:00
polish webui and add desktop gateway service support
This commit is contained in:
208
webui/src/components/Button.tsx
Normal file
208
webui/src/components/Button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user