mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Compare commits
8 Commits
@@ -50,7 +50,7 @@ function computeUnifiedDiff(original: string, modified: string): DiffResult {
|
||||
let totalAdditions = 0;
|
||||
let totalDeletions = 0;
|
||||
|
||||
const hunks: Hunk[] = chunks.map((chunk) => {
|
||||
const hunks: Hunk[] = chunks.map((chunk: Chunk) => {
|
||||
const lines: UnifiedLine[] = [];
|
||||
|
||||
const hasDel = chunk.fromA < chunk.toA;
|
||||
|
||||
@@ -13,15 +13,15 @@ import { Button } from '@/components/ui/Button';
|
||||
import { PageTransition } from '@/components/common/PageTransition';
|
||||
import { MainRoutes } from '@/router/MainRoutes';
|
||||
import {
|
||||
IconBot,
|
||||
IconChartLine,
|
||||
IconFileText,
|
||||
IconInfo,
|
||||
IconLayoutDashboard,
|
||||
IconScrollText,
|
||||
IconSettings,
|
||||
IconShield,
|
||||
IconTimer,
|
||||
IconSidebarAuthFiles,
|
||||
IconSidebarConfig,
|
||||
IconSidebarDashboard,
|
||||
IconSidebarLogs,
|
||||
IconSidebarOauth,
|
||||
IconSidebarProviders,
|
||||
IconSidebarQuota,
|
||||
IconSidebarSystem,
|
||||
IconSidebarUsage,
|
||||
} from '@/components/ui/icons';
|
||||
import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
|
||||
import {
|
||||
@@ -38,15 +38,15 @@ import { isSupportedLanguage } from '@/utils/language';
|
||||
import type { Theme } from '@/types';
|
||||
|
||||
const sidebarIcons: Record<string, ReactNode> = {
|
||||
dashboard: <IconLayoutDashboard size={18} />,
|
||||
aiProviders: <IconBot size={18} />,
|
||||
authFiles: <IconFileText size={18} />,
|
||||
oauth: <IconShield size={18} />,
|
||||
quota: <IconTimer size={18} />,
|
||||
usage: <IconChartLine size={18} />,
|
||||
config: <IconSettings size={18} />,
|
||||
logs: <IconScrollText size={18} />,
|
||||
system: <IconInfo size={18} />,
|
||||
dashboard: <IconSidebarDashboard size={18} />,
|
||||
aiProviders: <IconSidebarProviders size={18} />,
|
||||
authFiles: <IconSidebarAuthFiles size={18} />,
|
||||
oauth: <IconSidebarOauth size={18} />,
|
||||
quota: <IconSidebarQuota size={18} />,
|
||||
usage: <IconSidebarUsage size={18} />,
|
||||
config: <IconSidebarConfig size={18} />,
|
||||
logs: <IconSidebarLogs size={18} />,
|
||||
system: <IconSidebarSystem size={18} />,
|
||||
};
|
||||
|
||||
// Header action icons - smaller size for header buttons
|
||||
@@ -132,7 +132,13 @@ const headerIcons = {
|
||||
</clipPath>
|
||||
</defs>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<circle cx="12" cy="12" r="4" clipPath="url(#mainLayoutAutoThemeSunLeftHalf)" fill="currentColor" />
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="4"
|
||||
clipPath="url(#mainLayoutAutoThemeSunLeftHalf)"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="M4.93 4.93l1.41 1.41" />
|
||||
@@ -171,17 +177,35 @@ const THEME_CARDS: Array<{
|
||||
{
|
||||
key: 'white',
|
||||
labelKey: 'theme.white',
|
||||
colors: { bg: '#ffffff', card: '#ffffff', border: '#e5e5e5', text: '#2d2a26', textMuted: '#a29c95' },
|
||||
colors: {
|
||||
bg: '#ffffff',
|
||||
card: '#ffffff',
|
||||
border: '#e5e5e5',
|
||||
text: '#2d2a26',
|
||||
textMuted: '#a29c95',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'light',
|
||||
labelKey: 'theme.light',
|
||||
colors: { bg: '#faf9f5', card: '#f0eee8', border: '#e3e1db', text: '#2d2a26', textMuted: '#a29c95' },
|
||||
colors: {
|
||||
bg: '#faf9f5',
|
||||
card: '#f0eee8',
|
||||
border: '#e3e1db',
|
||||
text: '#2d2a26',
|
||||
textMuted: '#a29c95',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'dark',
|
||||
labelKey: 'theme.dark',
|
||||
colors: { bg: '#151412', card: '#1d1b18', border: '#3a3530', text: '#f6f4f1', textMuted: '#9c958d' },
|
||||
colors: {
|
||||
bg: '#151412',
|
||||
card: '#1d1b18',
|
||||
border: '#3a3530',
|
||||
text: '#f6f4f1',
|
||||
textMuted: '#9c958d',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -420,7 +444,6 @@ export function MainLayout() {
|
||||
});
|
||||
}, [fetchConfig]);
|
||||
|
||||
|
||||
const statusClass =
|
||||
connectionStatus === 'connected'
|
||||
? 'success'
|
||||
@@ -503,7 +526,7 @@ export function MainLayout() {
|
||||
clearCache();
|
||||
const results = await Promise.allSettled([
|
||||
fetchConfig(undefined, true),
|
||||
triggerHeaderRefresh()
|
||||
triggerHeaderRefresh(),
|
||||
]);
|
||||
const rejected = results.find((result) => result.status === 'rejected');
|
||||
if (rejected && rejected.status === 'rejected') {
|
||||
@@ -618,7 +641,10 @@ export function MainLayout() {
|
||||
>
|
||||
{headerIcons.update}
|
||||
</Button>
|
||||
<div className={`language-menu ${languageMenuOpen ? 'open' : ''}`} ref={languageMenuRef}>
|
||||
<div
|
||||
className={`language-menu ${languageMenuOpen ? 'open' : ''}`}
|
||||
ref={languageMenuRef}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -631,7 +657,11 @@ export function MainLayout() {
|
||||
{headerIcons.language}
|
||||
</Button>
|
||||
{languageMenuOpen && (
|
||||
<div className="notification entering language-menu-popover" role="menu" aria-label={t('language.switch')}>
|
||||
<div
|
||||
className="notification entering language-menu-popover"
|
||||
role="menu"
|
||||
aria-label={t('language.switch')}
|
||||
>
|
||||
{LANGUAGE_ORDER.map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
@@ -667,7 +697,11 @@ export function MainLayout() {
|
||||
: headerIcons.sun}
|
||||
</Button>
|
||||
{themeMenuOpen && (
|
||||
<div className="notification entering theme-menu-popover" role="menu" aria-label={t('theme.switch')}>
|
||||
<div
|
||||
className="notification entering theme-menu-popover"
|
||||
role="menu"
|
||||
aria-label={t('theme.switch')}
|
||||
>
|
||||
{THEME_CARDS.map((tc) => (
|
||||
<button
|
||||
key={tc.key}
|
||||
@@ -679,7 +713,10 @@ export function MainLayout() {
|
||||
>
|
||||
<div
|
||||
className="theme-card-preview"
|
||||
style={{ background: tc.colors.bg, border: `1px solid ${tc.colors.border}` }}
|
||||
style={{
|
||||
background: tc.colors.bg,
|
||||
border: `1px solid ${tc.colors.border}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="theme-card-header"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Fragment, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import iconVertex from '@/assets/icons/vertex.svg';
|
||||
import type { ProviderKeyConfig } from '@/types';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { ProviderList } from '../ProviderList';
|
||||
import { ProviderStatusBar } from '../ProviderStatusBar';
|
||||
import { getStatsBySource } from '../utils';
|
||||
import { getStatsBySource, hasDisableAllModelsRule } from '../utils';
|
||||
|
||||
interface VertexSectionProps {
|
||||
configs: ProviderKeyConfig[];
|
||||
@@ -26,6 +27,7 @@ interface VertexSectionProps {
|
||||
onAdd: () => void;
|
||||
onEdit: (index: number) => void;
|
||||
onDelete: (index: number) => void;
|
||||
onToggle: (index: number, enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export function VertexSection({
|
||||
@@ -38,9 +40,11 @@ export function VertexSection({
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onToggle,
|
||||
}: VertexSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const actionsDisabled = disableControls || loading || isSwitching;
|
||||
const toggleDisabled = disableControls || loading || isSwitching;
|
||||
|
||||
const statusBarCache = useMemo(() => {
|
||||
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
||||
@@ -84,9 +88,19 @@ export function VertexSection({
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
actionsDisabled={actionsDisabled}
|
||||
getRowDisabled={(item) => hasDisableAllModelsRule(item.excludedModels)}
|
||||
renderExtraActions={(item, index) => (
|
||||
<ToggleSwitch
|
||||
label={t('ai_providers.config_toggle_label')}
|
||||
checked={!hasDisableAllModelsRule(item.excludedModels)}
|
||||
disabled={toggleDisabled}
|
||||
onChange={(value) => void onToggle(index, value)}
|
||||
/>
|
||||
)}
|
||||
renderContent={(item, index) => {
|
||||
const stats = getStatsBySource(item.apiKey, keyStats, item.prefix);
|
||||
const headerEntries = Object.entries(item.headers || {});
|
||||
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
||||
const excludedModels = item.excludedModels ?? [];
|
||||
const statusData = statusBarCache.get(item.apiKey) || calculateStatusBarData([]);
|
||||
|
||||
@@ -126,6 +140,11 @@ export function VertexSection({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{configDisabled && (
|
||||
<div className="status-badge warning" style={{ marginTop: 8, marginBottom: 0 }}>
|
||||
{t('ai_providers.config_disabled_badge')}
|
||||
</div>
|
||||
)}
|
||||
{item.models?.length ? (
|
||||
<div className={styles.modelTagList}>
|
||||
<span className={styles.modelCountLabel}>
|
||||
|
||||
@@ -675,11 +675,16 @@ const buildClaudeQuotaWindows = (
|
||||
return windows;
|
||||
};
|
||||
|
||||
const CLAUDE_PLAN_TYPE_MAP: Record<string, string> = {
|
||||
default_claude_max_5x: 'plan_max5',
|
||||
default_claude_max_20x: 'plan_max20',
|
||||
default_claude_pro: 'plan_pro',
|
||||
default_claude_ai: 'plan_free',
|
||||
const normalizeFlagValue = (value: unknown): boolean | undefined => {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'number') return value !== 0;
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
if (['true', '1', 'yes', 'y', 'on'].includes(trimmed)) return true;
|
||||
if (['false', '0', 'no', 'n', 'off'].includes(trimmed)) return false;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const parseClaudeProfilePayload = (payload: unknown): ClaudeProfileResponse | null => {
|
||||
@@ -702,10 +707,15 @@ const parseClaudeProfilePayload = (payload: unknown): ClaudeProfileResponse | nu
|
||||
const resolveClaudePlanType = (profile: ClaudeProfileResponse | null): string | null => {
|
||||
if (!profile) return null;
|
||||
|
||||
const tier = normalizeStringValue(profile.organization?.rate_limit_tier);
|
||||
if (!tier) return null;
|
||||
const hasClaudeMax = normalizeFlagValue(profile.account?.has_claude_max);
|
||||
if (hasClaudeMax) return 'plan_max';
|
||||
|
||||
return CLAUDE_PLAN_TYPE_MAP[tier] ?? 'plan_unknown';
|
||||
const hasClaudePro = normalizeFlagValue(profile.account?.has_claude_pro);
|
||||
if (hasClaudePro) return 'plan_pro';
|
||||
|
||||
if (hasClaudeMax === false && hasClaudePro === false) return 'plan_free';
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const fetchClaudeQuota = async (
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
@use '../../styles/variables' as *;
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 92%, transparent);
|
||||
color: var(--primary-contrast, #fff);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
border-color $transition-fast,
|
||||
background-color $transition-fast,
|
||||
box-shadow $transition-fast,
|
||||
transform $transition-fast;
|
||||
}
|
||||
|
||||
.root:hover .box {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 16%, transparent);
|
||||
}
|
||||
|
||||
.root:active .box {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.disabled:hover .box {
|
||||
border-color: var(--border-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.disabled:active .box {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.input:focus-visible + .box {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow:
|
||||
0 0 0 3px color-mix(in srgb, var(--primary-color) 16%, transparent),
|
||||
0 0 0 1px color-mix(in srgb, var(--primary-color) 50%, transparent);
|
||||
}
|
||||
|
||||
.boxChecked {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.boxChecked svg {
|
||||
display: block;
|
||||
stroke-width: 2.4;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ChangeEvent, ReactNode } from 'react';
|
||||
import { IconCheck } from './icons';
|
||||
import styles from './SelectionCheckbox.module.scss';
|
||||
|
||||
interface SelectionCheckboxProps {
|
||||
checked: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label?: ReactNode;
|
||||
ariaLabel?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
}
|
||||
|
||||
export function SelectionCheckbox({
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
ariaLabel,
|
||||
title,
|
||||
disabled = false,
|
||||
className,
|
||||
labelClassName,
|
||||
}: SelectionCheckboxProps) {
|
||||
const rootClassName = [styles.root, disabled ? styles.disabled : '', className]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const boxClassName = [styles.box, checked ? styles.boxChecked : ''].filter(Boolean).join(' ');
|
||||
const textClassName = [styles.label, labelClassName].filter(Boolean).join(' ');
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(event.target.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<label className={rootClassName} title={title}>
|
||||
<input
|
||||
className={styles.input}
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<span className={boxClassName}>{checked ? <IconCheck size={12} /> : null}</span>
|
||||
{label ? <span className={textClassName}>{label}</span> : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
+191
-1
@@ -16,7 +16,15 @@ const baseSvgProps: SVGProps<SVGSVGElement> = {
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
'aria-hidden': 'true',
|
||||
focusable: 'false'
|
||||
focusable: 'false',
|
||||
};
|
||||
|
||||
const sidebarSvgProps: SVGProps<SVGSVGElement> = {
|
||||
...baseSvgProps,
|
||||
strokeWidth: 1.72,
|
||||
strokeLinecap: 'square',
|
||||
strokeLinejoin: 'miter',
|
||||
strokeMiterlimit: 10,
|
||||
};
|
||||
|
||||
export function IconSlidersHorizontal({ size = 20, ...props }: IconProps) {
|
||||
@@ -322,3 +330,185 @@ export function IconLayoutDashboard({ size = 20, ...props }: IconProps) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSidebarDashboard({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...sidebarSvgProps} width={size} height={size} {...props}>
|
||||
<rect x="3.75" y="4.5" width="16.5" height="15" rx="1.5" />
|
||||
<path d="M3.75 9.25h16.5" />
|
||||
<path d="M10.5 9.25V19.5" />
|
||||
<rect
|
||||
x="6.1"
|
||||
y="12.1"
|
||||
width="2.3"
|
||||
height="2.3"
|
||||
rx="0.35"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.16"
|
||||
/>
|
||||
<polyline points="13.1 15.8 15.2 13.6 16.8 15 18.35 11.95" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSidebarConfig({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...sidebarSvgProps} width={size} height={size} {...props}>
|
||||
<path d="M5 8h14" />
|
||||
<path d="M5 16h14" />
|
||||
<path d="M9 6.1 10.9 8 9 9.9 7.1 8Z" fill="currentColor" fillOpacity="0.16" />
|
||||
<rect
|
||||
x="13.6"
|
||||
y="14.1"
|
||||
width="2.9"
|
||||
height="2.9"
|
||||
rx="0.45"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.16"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSidebarProviders({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...sidebarSvgProps} width={size} height={size} {...props}>
|
||||
<path d="M12 4.7 14.8 7.5 12 10.3 9.2 7.5Z" fill="currentColor" fillOpacity="0.16" />
|
||||
<rect x="4.6" y="13" width="3.8" height="3.8" rx="0.5" />
|
||||
<rect x="15.6" y="13" width="3.8" height="3.8" rx="0.5" />
|
||||
<rect x="10.1" y="16.5" width="3.8" height="3.8" rx="0.5" />
|
||||
<path d="M12 10.3v6.2" />
|
||||
<path d="M12 10.3 6.5 13" />
|
||||
<path d="M12 10.3 17.5 13" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSidebarAuthFiles({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...sidebarSvgProps} width={size} height={size} {...props}>
|
||||
<path d="M7 4.5h7l3 3v11a2.5 2.5 0 0 1-2.5 2.5H7.5A2.5 2.5 0 0 1 5 18.5V7a2.5 2.5 0 0 1 2-2.5Z" />
|
||||
<path d="M14 4.5v3h3" />
|
||||
<path d="M8.4 10.9h5.7" />
|
||||
<path d="M8.4 14.2h4.4" />
|
||||
<path d="M15.9 14.6 18.3 17 15.9 19.4 13.5 17Z" fill="currentColor" fillOpacity="0.16" />
|
||||
<path d="m14.9 17 0.9 0.9 1.8-1.9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSidebarOauth({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...sidebarSvgProps} width={size} height={size} {...props}>
|
||||
<path d="M4.5 8.5h8.2" />
|
||||
<polyline points="10.1 5.6 13 8.5 10.1 11.4" />
|
||||
<path d="M19.5 15.5h-8.2" />
|
||||
<polyline points="13.9 12.6 11 15.5 13.9 18.4" />
|
||||
<path d="M12 9.4 14.6 12 12 14.6 9.4 12Z" fill="currentColor" fillOpacity="0.16" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSidebarQuota({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...sidebarSvgProps} width={size} height={size} {...props}>
|
||||
<path d="M5 16.8a7 7 0 0 1 14 0" />
|
||||
<path d="m7.3 13.8 1.4-1.4" />
|
||||
<path d="M12 11V9" />
|
||||
<path d="m16.7 13.8-1.4-1.4" />
|
||||
<path d="M12 16.8 15.5 12.4" />
|
||||
<path d="M12 15.2 13.6 16.8 12 18.4 10.4 16.8Z" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSidebarUsage({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...sidebarSvgProps} width={size} height={size} {...props}>
|
||||
<path d="M5 5v14a2 2 0 0 0 2 2h12" />
|
||||
<polyline points="7.4 15.5 10.2 12.3 12.7 13.8 16.1 9.1 18.4 10.8" />
|
||||
<rect
|
||||
x="9.55"
|
||||
y="11.65"
|
||||
width="1.3"
|
||||
height="1.3"
|
||||
rx="0.2"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
/>
|
||||
<rect
|
||||
x="12.05"
|
||||
y="13.15"
|
||||
width="1.3"
|
||||
height="1.3"
|
||||
rx="0.2"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
/>
|
||||
<rect
|
||||
x="15.45"
|
||||
y="8.45"
|
||||
width="1.3"
|
||||
height="1.3"
|
||||
rx="0.2"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSidebarLogs({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...sidebarSvgProps} width={size} height={size} {...props}>
|
||||
<rect x="4" y="5" width="16" height="14" rx="1.5" />
|
||||
<path d="M4 9h16" />
|
||||
<rect
|
||||
x="6.1"
|
||||
y="6.35"
|
||||
width="1.15"
|
||||
height="1.15"
|
||||
rx="0.15"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
/>
|
||||
<rect
|
||||
x="8.55"
|
||||
y="6.35"
|
||||
width="1.15"
|
||||
height="1.15"
|
||||
rx="0.15"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.45"
|
||||
stroke="none"
|
||||
/>
|
||||
<path d="m7.1 12.3 2.5 2-2.5 2" />
|
||||
<path d="M11.9 12.2h3.4" />
|
||||
<path d="M11.9 16.4h4.8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconSidebarSystem({ size = 20, ...props }: IconProps) {
|
||||
return (
|
||||
<svg {...sidebarSvgProps} width={size} height={size} {...props}>
|
||||
<rect x="5" y="5" width="14" height="3.3" rx="0.8" />
|
||||
<rect x="5" y="10.35" width="14" height="3.3" rx="0.8" />
|
||||
<rect x="5" y="15.7" width="14" height="3.3" rx="0.8" />
|
||||
<rect x="6.8" y="6.05" width="1.1" height="1.1" rx="0.15" fill="currentColor" stroke="none" />
|
||||
<rect x="6.8" y="11.4" width="1.1" height="1.1" rx="0.15" fill="currentColor" stroke="none" />
|
||||
<rect
|
||||
x="6.8"
|
||||
y="16.75"
|
||||
width="1.1"
|
||||
height="1.1"
|
||||
rx="0.15"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
/>
|
||||
<path d="M10.4 6.6h5.2" />
|
||||
<path d="M10.4 11.95h5.2" />
|
||||
<path d="M10.4 17.3h5.2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||
import { SelectionCheckbox } from '@/components/ui/SelectionCheckbox';
|
||||
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import {
|
||||
IconBot,
|
||||
IconCheck,
|
||||
IconCode,
|
||||
IconDownload,
|
||||
IconInfo,
|
||||
@@ -118,18 +118,14 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
<div className={styles.fileCardMain}>
|
||||
<div className={styles.cardHeader}>
|
||||
{!isRuntimeOnly && (
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.selectionToggle} ${selected ? styles.selectionToggleActive : ''}`}
|
||||
onClick={() => onToggleSelect(file.name)}
|
||||
<SelectionCheckbox
|
||||
checked={selected}
|
||||
onChange={() => onToggleSelect(file.name)}
|
||||
aria-label={
|
||||
selected ? t('auth_files.batch_deselect') : t('auth_files.batch_select_all')
|
||||
}
|
||||
aria-pressed={selected}
|
||||
title={selected ? t('auth_files.batch_deselect') : t('auth_files.batch_select_all')}
|
||||
>
|
||||
{selected && <IconCheck size={12} />}
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={styles.typeBadge}
|
||||
|
||||
@@ -342,18 +342,17 @@
|
||||
"vertex_add_modal_title": "Add Vertex API Configuration",
|
||||
"vertex_add_modal_key_label": "API Key:",
|
||||
"vertex_add_modal_key_placeholder": "Please enter Vertex API key",
|
||||
"vertex_add_modal_url_label": "Base URL (Required):",
|
||||
"vertex_add_modal_url_label": "Base URL:",
|
||||
"vertex_add_modal_url_placeholder": "e.g.: https://example.com/api",
|
||||
"vertex_add_modal_proxy_label": "Proxy URL (Optional):",
|
||||
"vertex_add_modal_proxy_placeholder": "e.g.: socks5://proxy.example.com:1080",
|
||||
"vertex_edit_modal_title": "Edit Vertex API Configuration",
|
||||
"vertex_edit_modal_key_label": "API Key:",
|
||||
"vertex_edit_modal_url_label": "Base URL (Required):",
|
||||
"vertex_edit_modal_url_label": "Base URL:",
|
||||
"vertex_edit_modal_proxy_label": "Proxy URL (Optional):",
|
||||
"vertex_delete_confirm": "Are you sure you want to delete this Vertex configuration?",
|
||||
"vertex_models_label": "Model aliases (alias required):",
|
||||
"vertex_models_label": "Model aliases:",
|
||||
"vertex_models_add_btn": "Add Mapping",
|
||||
"vertex_models_hint": "Each alias needs both the original model and the alias.",
|
||||
"vertex_models_count": "Alias count",
|
||||
"ampcode_title": "Amp CLI Integration (ampcode)",
|
||||
"ampcode_modal_title": "Configure Ampcode",
|
||||
@@ -605,6 +604,7 @@
|
||||
"plan_unknown": "Unknown",
|
||||
"plan_free": "Free",
|
||||
"plan_pro": "Pro",
|
||||
"plan_max": "Max",
|
||||
"plan_max5": "Max 5x",
|
||||
"plan_max20": "Max 20x"
|
||||
},
|
||||
@@ -1373,7 +1373,6 @@
|
||||
"vertex_config_added": "Vertex configuration added successfully",
|
||||
"vertex_config_updated": "Vertex configuration updated successfully",
|
||||
"vertex_config_deleted": "Vertex configuration deleted successfully",
|
||||
"vertex_base_url_required": "Please enter the Vertex Base URL",
|
||||
"config_enabled": "Configuration enabled",
|
||||
"config_disabled": "Configuration disabled",
|
||||
"field_required": "Required fields cannot be empty",
|
||||
|
||||
@@ -342,18 +342,17 @@
|
||||
"vertex_add_modal_title": "Добавление конфигурации Vertex API",
|
||||
"vertex_add_modal_key_label": "API-ключ:",
|
||||
"vertex_add_modal_key_placeholder": "Введите API-ключ Vertex",
|
||||
"vertex_add_modal_url_label": "Базовый URL (обязательно):",
|
||||
"vertex_add_modal_url_label": "Базовый URL:",
|
||||
"vertex_add_modal_url_placeholder": "например: https://example.com/api",
|
||||
"vertex_add_modal_proxy_label": "URL прокси (необязательно):",
|
||||
"vertex_add_modal_proxy_placeholder": "например: socks5://proxy.example.com:1080",
|
||||
"vertex_edit_modal_title": "Редактирование конфигурации Vertex API",
|
||||
"vertex_edit_modal_key_label": "API-ключ:",
|
||||
"vertex_edit_modal_url_label": "Базовый URL (обязательно):",
|
||||
"vertex_edit_modal_url_label": "Базовый URL:",
|
||||
"vertex_edit_modal_proxy_label": "URL прокси (необязательно):",
|
||||
"vertex_delete_confirm": "Удалить эту конфигурацию Vertex?",
|
||||
"vertex_models_label": "Псевдонимы моделей (требуется псевдоним):",
|
||||
"vertex_models_label": "Псевдонимы моделей:",
|
||||
"vertex_models_add_btn": "Добавить сопоставление",
|
||||
"vertex_models_hint": "Каждому псевдониму требуются исходная модель и псевдоним.",
|
||||
"vertex_models_count": "Количество псевдонимов",
|
||||
"ampcode_title": "Интеграция Amp CLI (ampcode)",
|
||||
"ampcode_modal_title": "Настройка Ampcode",
|
||||
@@ -608,6 +607,7 @@
|
||||
"plan_unknown": "Неизвестно",
|
||||
"plan_free": "Free",
|
||||
"plan_pro": "Pro",
|
||||
"plan_max": "Max",
|
||||
"plan_max5": "Max 5x",
|
||||
"plan_max20": "Max 20x"
|
||||
},
|
||||
@@ -1378,7 +1378,6 @@
|
||||
"vertex_config_added": "Конфигурация Vertex успешно добавлена",
|
||||
"vertex_config_updated": "Конфигурация Vertex успешно обновлена",
|
||||
"vertex_config_deleted": "Конфигурация Vertex успешно удалена",
|
||||
"vertex_base_url_required": "Введите базовый URL Vertex",
|
||||
"config_enabled": "Конфигурация включена",
|
||||
"config_disabled": "Конфигурация выключена",
|
||||
"field_required": "Обязательные поля не могут быть пустыми",
|
||||
|
||||
@@ -342,18 +342,17 @@
|
||||
"vertex_add_modal_title": "添加Vertex API配置",
|
||||
"vertex_add_modal_key_label": "API密钥:",
|
||||
"vertex_add_modal_key_placeholder": "请输入Vertex API密钥",
|
||||
"vertex_add_modal_url_label": "Base URL (必填):",
|
||||
"vertex_add_modal_url_label": "Base URL:",
|
||||
"vertex_add_modal_url_placeholder": "例如: https://example.com/api",
|
||||
"vertex_add_modal_proxy_label": "代理 URL (可选):",
|
||||
"vertex_add_modal_proxy_placeholder": "例如: socks5://proxy.example.com:1080",
|
||||
"vertex_edit_modal_title": "编辑Vertex API配置",
|
||||
"vertex_edit_modal_key_label": "API密钥:",
|
||||
"vertex_edit_modal_url_label": "Base URL (必填):",
|
||||
"vertex_edit_modal_url_label": "Base URL:",
|
||||
"vertex_edit_modal_proxy_label": "代理 URL (可选):",
|
||||
"vertex_delete_confirm": "确定要删除这个Vertex配置吗?",
|
||||
"vertex_models_label": "模型别名 (别名必填):",
|
||||
"vertex_models_label": "模型别名:",
|
||||
"vertex_models_add_btn": "添加映射",
|
||||
"vertex_models_hint": "每条别名需要填写原模型与别名。",
|
||||
"vertex_models_count": "别名数量",
|
||||
"ampcode_title": "Amp CLI 集成 (ampcode)",
|
||||
"ampcode_modal_title": "配置 Ampcode",
|
||||
@@ -605,6 +604,7 @@
|
||||
"plan_unknown": "未知",
|
||||
"plan_free": "免费版",
|
||||
"plan_pro": "专业版",
|
||||
"plan_max": "Max",
|
||||
"plan_max5": "Max 5x",
|
||||
"plan_max20": "Max 20x"
|
||||
},
|
||||
@@ -1373,7 +1373,6 @@
|
||||
"vertex_config_added": "Vertex配置添加成功",
|
||||
"vertex_config_updated": "Vertex配置更新成功",
|
||||
"vertex_config_deleted": "Vertex配置删除成功",
|
||||
"vertex_base_url_required": "请填写Vertex Base URL",
|
||||
"config_enabled": "配置已启用",
|
||||
"config_disabled": "配置已停用",
|
||||
"field_required": "必填字段不能为空",
|
||||
|
||||
@@ -164,7 +164,7 @@ export function AiProvidersPage() {
|
||||
};
|
||||
|
||||
const setConfigEnabled = async (
|
||||
provider: 'gemini' | 'codex' | 'claude',
|
||||
provider: 'gemini' | 'codex' | 'claude' | 'vertex',
|
||||
index: number,
|
||||
enabled: boolean
|
||||
) => {
|
||||
@@ -204,7 +204,12 @@ export function AiProvidersPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = provider === 'codex' ? codexConfigs : claudeConfigs;
|
||||
const source =
|
||||
provider === 'codex'
|
||||
? codexConfigs
|
||||
: provider === 'claude'
|
||||
? claudeConfigs
|
||||
: vertexConfigs;
|
||||
const current = source[index];
|
||||
if (!current) return;
|
||||
|
||||
@@ -222,17 +227,23 @@ export function AiProvidersPage() {
|
||||
setCodexConfigs(nextList);
|
||||
updateConfigValue('codex-api-key', nextList);
|
||||
clearCache('codex-api-key');
|
||||
} else {
|
||||
} else if (provider === 'claude') {
|
||||
setClaudeConfigs(nextList);
|
||||
updateConfigValue('claude-api-key', nextList);
|
||||
clearCache('claude-api-key');
|
||||
} else {
|
||||
setVertexConfigs(nextList);
|
||||
updateConfigValue('vertex-api-key', nextList);
|
||||
clearCache('vertex-api-key');
|
||||
}
|
||||
|
||||
try {
|
||||
if (provider === 'codex') {
|
||||
await providersApi.saveCodexConfigs(nextList);
|
||||
} else {
|
||||
} else if (provider === 'claude') {
|
||||
await providersApi.saveClaudeConfigs(nextList);
|
||||
} else {
|
||||
await providersApi.saveVertexConfigs(nextList);
|
||||
}
|
||||
showNotification(
|
||||
enabled ? t('notification.config_enabled') : t('notification.config_disabled'),
|
||||
@@ -244,10 +255,14 @@ export function AiProvidersPage() {
|
||||
setCodexConfigs(previousList);
|
||||
updateConfigValue('codex-api-key', previousList);
|
||||
clearCache('codex-api-key');
|
||||
} else {
|
||||
} else if (provider === 'claude') {
|
||||
setClaudeConfigs(previousList);
|
||||
updateConfigValue('claude-api-key', previousList);
|
||||
clearCache('claude-api-key');
|
||||
} else {
|
||||
setVertexConfigs(previousList);
|
||||
updateConfigValue('vertex-api-key', previousList);
|
||||
clearCache('vertex-api-key');
|
||||
}
|
||||
showNotification(`${t('notification.update_failed')}: ${message}`, 'error');
|
||||
} finally {
|
||||
@@ -400,6 +415,7 @@ export function AiProvidersPage() {
|
||||
onAdd={() => openEditor('/ai-providers/vertex/new')}
|
||||
onEdit={(index) => openEditor(`/ai-providers/vertex/${index}`)}
|
||||
onDelete={deleteVertex}
|
||||
onToggle={(index, enabled) => void setConfigEnabled('vertex', index, enabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -192,10 +192,6 @@ export function AiProvidersVertexEditPage() {
|
||||
|
||||
const trimmedBaseUrl = (form.baseUrl ?? '').trim();
|
||||
const baseUrl = trimmedBaseUrl || undefined;
|
||||
if (!baseUrl) {
|
||||
showNotification(t('notification.vertex_base_url_required'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setError('');
|
||||
@@ -347,7 +343,6 @@ export function AiProvidersVertexEditPage() {
|
||||
removeButtonAriaLabel={t('common.delete')}
|
||||
disabled={disableControls || saving}
|
||||
/>
|
||||
<div className="hint">{t('ai_providers.vertex_models_hint')}</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>{t('ai_providers.excluded_models_label')}</label>
|
||||
|
||||
@@ -565,45 +565,6 @@
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.selectionToggle {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: color-mix(in srgb, var(--bg-secondary) 92%, transparent);
|
||||
color: var(--primary-contrast, #fff);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color $transition-fast,
|
||||
background-color $transition-fast,
|
||||
box-shadow $transition-fast,
|
||||
transform $transition-fast;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 16%, transparent);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.selectionToggleActive {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.selectionToggleActive svg {
|
||||
display: block;
|
||||
stroke-width: 2.4;
|
||||
}
|
||||
|
||||
.typeBadge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Select } from '@/components/ui/Select';
|
||||
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import { SelectionCheckbox } from '@/components/ui/SelectionCheckbox';
|
||||
import { IconEye, IconEyeOff } from '@/components/ui/icons';
|
||||
import { useAuthStore, useLanguageStore, useNotificationStore } from '@/stores';
|
||||
import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection';
|
||||
@@ -233,11 +233,12 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className={styles.toggleAdvanced}>
|
||||
<ToggleSwitch
|
||||
<SelectionCheckbox
|
||||
checked={showCustomBase}
|
||||
onChange={setShowCustomBase}
|
||||
ariaLabel={t('login.custom_connection_label')}
|
||||
label={<span className={styles.toggleLabel}>{t('login.custom_connection_label')}</span>}
|
||||
label={t('login.custom_connection_label')}
|
||||
labelClassName={styles.toggleLabel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -281,11 +282,12 @@ export function LoginPage() {
|
||||
/>
|
||||
|
||||
<div className={styles.toggleAdvanced}>
|
||||
<ToggleSwitch
|
||||
<SelectionCheckbox
|
||||
checked={rememberPassword}
|
||||
onChange={setRememberPassword}
|
||||
ariaLabel={t('login.remember_password_label')}
|
||||
label={<span className={styles.toggleLabel}>{t('login.remember_password_label')}</span>}
|
||||
label={t('login.remember_password_label')}
|
||||
labelClassName={styles.toggleLabel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { STORAGE_KEY_AUTH } from '@/utils/constants';
|
||||
import { secureStorage } from '@/services/storage/secureStorage';
|
||||
import { apiClient } from '@/services/api/client';
|
||||
import { useConfigStore } from './useConfigStore';
|
||||
import { useUsageStatsStore } from './useUsageStatsStore';
|
||||
import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection';
|
||||
|
||||
interface AuthStoreState extends AuthState {
|
||||
@@ -136,6 +137,7 @@ export const useAuthStore = create<AuthStoreState>()(
|
||||
logout: () => {
|
||||
restoreSessionPromise = null;
|
||||
useConfigStore.getState().clearCache();
|
||||
useUsageStatsStore.getState().clearUsageStats();
|
||||
set({
|
||||
isAuthenticated: false,
|
||||
apiBase: '',
|
||||
|
||||
+46
-10
@@ -81,7 +81,9 @@
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
transition: background $transition-fast, color $transition-fast;
|
||||
transition:
|
||||
background $transition-fast,
|
||||
color $transition-fast;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -118,7 +120,10 @@
|
||||
max-width: 320px;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: max-width 0.4s ease, opacity 0.4s ease, transform 0.4s ease;
|
||||
transition:
|
||||
max-width 0.4s ease,
|
||||
opacity 0.4s ease,
|
||||
transform 0.4s ease;
|
||||
}
|
||||
|
||||
.brand-abbr {
|
||||
@@ -126,7 +131,9 @@
|
||||
transform: translateX(12px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.4s ease, transform 0.4s ease;
|
||||
transition:
|
||||
opacity 0.4s ease,
|
||||
transform 0.4s ease;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
@@ -220,7 +227,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: background-color $transition-fast, color $transition-fast;
|
||||
transition:
|
||||
background-color $transition-fast,
|
||||
color $transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
@@ -278,7 +287,9 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: border-color $transition-fast, background-color $transition-fast;
|
||||
transition:
|
||||
border-color $transition-fast,
|
||||
background-color $transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
@@ -439,7 +450,9 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-lg;
|
||||
transition: width $transition-normal, transform $transition-normal;
|
||||
transition:
|
||||
width $transition-normal,
|
||||
transform $transition-normal;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
@@ -464,22 +477,33 @@
|
||||
.nav-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: $radius-md;
|
||||
border: 1px solid transparent;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
cursor: pointer;
|
||||
transition: background $transition-fast, color $transition-fast;
|
||||
transition:
|
||||
background $transition-fast,
|
||||
color $transition-fast;
|
||||
|
||||
.nav-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.9;
|
||||
opacity: 0.96;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(180deg, rgb(255 255 255 / 0.18), rgb(255 255 255 / 0))
|
||||
var(--bg-secondary);
|
||||
box-shadow: inset 0 0 0 1px var(--border-primary);
|
||||
transition:
|
||||
background $transition-fast,
|
||||
box-shadow $transition-fast,
|
||||
color $transition-fast;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
@@ -496,12 +520,24 @@
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-secondary);
|
||||
|
||||
.nav-icon {
|
||||
background: linear-gradient(180deg, rgb(255 255 255 / 0.24), rgb(255 255 255 / 0))
|
||||
var(--bg-primary);
|
||||
box-shadow: inset 0 0 0 1px var(--border-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba($primary-color, 0.14);
|
||||
color: var(--primary-color);
|
||||
border: 1px solid rgba($primary-color, 0.35);
|
||||
|
||||
.nav-icon {
|
||||
background: linear-gradient(180deg, rgb(255 255 255 / 0.22), rgb(255 255 255 / 0))
|
||||
rgba($primary-color, 0.1);
|
||||
box-shadow: inset 0 0 0 1px rgba($primary-color, 0.26);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user