mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
UI updates based on impeccable passes
This commit is contained in:
parent
bbea8a537b
commit
8c1082627f
36 changed files with 320 additions and 325 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -3,6 +3,8 @@ dist/
|
||||||
dist-electron/
|
dist-electron/
|
||||||
out/
|
out/
|
||||||
release/
|
release/
|
||||||
|
.agents/
|
||||||
|
.impeccable.md
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "gerbil",
|
"name": "gerbil",
|
||||||
"version": "1.22.1",
|
"version": "1.23.0",
|
||||||
"description": "Run Large Language Models locally",
|
"description": "Run Large Language Models locally",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ai",
|
"ai",
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
"@codemirror/search": "^6.6.0",
|
"@codemirror/search": "^6.6.0",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@codemirror/view": "^6.41.0",
|
"@codemirror/view": "^6.41.0",
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/geist": "^5.2.8",
|
||||||
"@huggingface/gguf": "^0.4.2",
|
"@huggingface/gguf": "^0.4.2",
|
||||||
"@mantine/core": "^9.0.1",
|
"@mantine/core": "^9.0.1",
|
||||||
"@mantine/hooks": "^9.0.1",
|
"@mantine/hooks": "^9.0.1",
|
||||||
|
|
|
||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -42,7 +42,7 @@ importers:
|
||||||
'@codemirror/view':
|
'@codemirror/view':
|
||||||
specifier: ^6.41.0
|
specifier: ^6.41.0
|
||||||
version: 6.41.0
|
version: 6.41.0
|
||||||
'@fontsource/inter':
|
'@fontsource/geist':
|
||||||
specifier: ^5.2.8
|
specifier: ^5.2.8
|
||||||
version: 5.2.8
|
version: 5.2.8
|
||||||
'@huggingface/gguf':
|
'@huggingface/gguf':
|
||||||
|
|
@ -489,8 +489,8 @@ packages:
|
||||||
'@floating-ui/utils@0.2.11':
|
'@floating-ui/utils@0.2.11':
|
||||||
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
||||||
|
|
||||||
'@fontsource/inter@5.2.8':
|
'@fontsource/geist@5.2.8':
|
||||||
resolution: {integrity: sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==}
|
resolution: {integrity: sha512-pI54klK6vz8fVpAVyV+iODGLEzw73cs1XJqV9PIXgW8MYMmBcwK6lKKaWqJrNHwEx8ppz9cuhussb6arXQ/PVQ==}
|
||||||
|
|
||||||
'@huggingface/gguf@0.4.2':
|
'@huggingface/gguf@0.4.2':
|
||||||
resolution: {integrity: sha512-pog57zGj/u8uZ6cFi2vH9MkWvq+ZtFDa5iUeNxggDSle+04htJQFbl31TcauGUhnxTBJokFexWK9WL2BakLR0Q==}
|
resolution: {integrity: sha512-pog57zGj/u8uZ6cFi2vH9MkWvq+ZtFDa5iUeNxggDSle+04htJQFbl31TcauGUhnxTBJokFexWK9WL2BakLR0Q==}
|
||||||
|
|
@ -3530,7 +3530,7 @@ snapshots:
|
||||||
|
|
||||||
'@floating-ui/utils@0.2.11': {}
|
'@floating-ui/utils@0.2.11': {}
|
||||||
|
|
||||||
'@fontsource/inter@5.2.8': {}
|
'@fontsource/geist@5.2.8': {}
|
||||||
|
|
||||||
'@huggingface/gguf@0.4.2':
|
'@huggingface/gguf@0.4.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,17 @@
|
||||||
import { ActionIcon, Button, Tooltip } from '@mantine/core';
|
import { ActionIcon, Button, Tooltip } from '@mantine/core';
|
||||||
import { Activity } from 'lucide-react';
|
import { Activity } from 'lucide-react';
|
||||||
|
|
||||||
|
const BADGE_STYLE = {
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
fontSize: '0.7em',
|
||||||
|
fontWeight: 500,
|
||||||
|
height: 'auto',
|
||||||
|
margin: '0.125rem 0',
|
||||||
|
minWidth: '5rem',
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
} as const;
|
||||||
|
|
||||||
interface PerformanceBadgeProps {
|
interface PerformanceBadgeProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
|
@ -25,7 +36,12 @@ export const PerformanceBadge = ({
|
||||||
if (iconOnly) {
|
if (iconOnly) {
|
||||||
return (
|
return (
|
||||||
<Tooltip label={tooltipLabel} position="top">
|
<Tooltip label={tooltipLabel} position="top">
|
||||||
<ActionIcon size="sm" variant="subtle" onClick={() => void handlePerformanceClick()}>
|
<ActionIcon
|
||||||
|
size="sm"
|
||||||
|
variant="subtle"
|
||||||
|
aria-label="View performance details"
|
||||||
|
onClick={() => void handlePerformanceClick()}
|
||||||
|
>
|
||||||
<Activity size="1.125rem" />
|
<Activity size="1.125rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -37,16 +53,7 @@ export const PerformanceBadge = ({
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="light"
|
variant="light"
|
||||||
style={{
|
style={BADGE_STYLE}
|
||||||
borderRadius: '0.75rem',
|
|
||||||
fontSize: '0.7em',
|
|
||||||
fontWeight: 500,
|
|
||||||
height: 'auto',
|
|
||||||
margin: '0.125rem 0',
|
|
||||||
minWidth: '5rem',
|
|
||||||
padding: '0.25rem 0.5rem',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
onClick={() => void handlePerformanceClick()}
|
onClick={() => void handlePerformanceClick()}
|
||||||
>
|
>
|
||||||
{label}: {value}
|
{label}: {value}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const AppRouter = ({
|
||||||
const isInterfaceScreen = currentScreen === 'interface';
|
const isInterfaceScreen = currentScreen === 'interface';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<main style={{ display: 'contents' }}>
|
||||||
<ScreenTransition isActive={currentScreen === 'welcome'} shouldAnimate={hasInitialized}>
|
<ScreenTransition isActive={currentScreen === 'welcome'} shouldAnimate={hasInitialized}>
|
||||||
<WelcomeScreen onGetStarted={onWelcomeComplete} />
|
<WelcomeScreen onGetStarted={onWelcomeComplete} />
|
||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
|
|
@ -43,6 +43,6 @@ export const AppRouter = ({
|
||||||
<ScreenTransition isActive={isInterfaceScreen} shouldAnimate={hasInitialized}>
|
<ScreenTransition isActive={isInterfaceScreen} shouldAnimate={hasInitialized}>
|
||||||
<InterfaceScreen activeTab={activeInterfaceTab} isServerReady={isServerReady} />
|
<InterfaceScreen activeTab={activeInterfaceTab} isServerReady={isServerReady} />
|
||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
</>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@ export const ScreenTransition = ({ isActive, shouldAnimate, children }: ScreenTr
|
||||||
transition="fade"
|
transition="fade"
|
||||||
duration={shouldAnimate ? 100 : 0}
|
duration={shouldAnimate ? 100 : 0}
|
||||||
timingFunction="ease-out"
|
timingFunction="ease-out"
|
||||||
|
keepMounted={false}
|
||||||
>
|
>
|
||||||
{(styles) => (
|
{(styles) => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
...styles,
|
...styles,
|
||||||
}}
|
}}
|
||||||
|
inert={!isActive ? true : undefined}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,8 @@ export const StatusBar = () => {
|
||||||
const [memoryMetrics, setMemoryMetrics] = useState<MemoryMetrics | null>(null);
|
const [memoryMetrics, setMemoryMetrics] = useState<MemoryMetrics | null>(null);
|
||||||
const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null);
|
const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null);
|
||||||
const [tunnelBaseUrl, setTunnelBaseUrl] = useState<string | null>(null);
|
const [tunnelBaseUrl, setTunnelBaseUrl] = useState<string | null>(null);
|
||||||
const {
|
const { systemMonitoringEnabled, frontendPreference, imageGenerationFrontendPreference } =
|
||||||
resolvedColorScheme: colorScheme,
|
usePreferencesStore();
|
||||||
systemMonitoringEnabled,
|
|
||||||
frontendPreference,
|
|
||||||
imageGenerationFrontendPreference,
|
|
||||||
} = usePreferencesStore();
|
|
||||||
const { isVisible, setVisible } = useNotepadStore();
|
const { isVisible, setVisible } = useNotepadStore();
|
||||||
const { isImageGenerationMode } = useLaunchConfigStore();
|
const { isImageGenerationMode } = useLaunchConfigStore();
|
||||||
|
|
||||||
|
|
@ -100,13 +96,16 @@ export const StatusBar = () => {
|
||||||
gap="xs"
|
gap="xs"
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
h="100%"
|
h="100%"
|
||||||
bg={colorScheme === 'dark' ? 'dark.6' : 'gray.1'}
|
style={{
|
||||||
|
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6))',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Tooltip label="Notepad" disabled={isVisible}>
|
<Tooltip label="Notepad" disabled={isVisible}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={isVisible ? 'filled' : 'subtle'}
|
variant={isVisible ? 'filled' : 'subtle'}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
aria-label="Toggle notepad"
|
||||||
onClick={() => setVisible(!isVisible)}
|
onClick={() => setVisible(!isVisible)}
|
||||||
>
|
>
|
||||||
<NotepadText size="1.25rem" />
|
<NotepadText size="1.25rem" />
|
||||||
|
|
@ -120,6 +119,7 @@ export const StatusBar = () => {
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="sm"
|
size="sm"
|
||||||
color={copied ? 'teal' : undefined}
|
color={copied ? 'teal' : undefined}
|
||||||
|
aria-label={copied ? 'Copied!' : 'Copy tunnel URL'}
|
||||||
onClick={copy}
|
onClick={copy}
|
||||||
>
|
>
|
||||||
{copied ? <Check size="1.25rem" /> : <Globe size="1.25rem" />}
|
{copied ? <Check size="1.25rem" /> : <Globe size="1.25rem" />}
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,7 @@ const renderOption = ({ option }: { option: SelectOption }) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: TitleBarProps) => {
|
export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: TitleBarProps) => {
|
||||||
const {
|
const { frontendPreference, imageGenerationFrontendPreference } = usePreferencesStore();
|
||||||
resolvedColorScheme: colorScheme,
|
|
||||||
frontendPreference,
|
|
||||||
imageGenerationFrontendPreference,
|
|
||||||
} = usePreferencesStore();
|
|
||||||
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
|
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
|
||||||
const [isMaximized, setIsMaximized] = useState(false);
|
const [isMaximized, setIsMaximized] = useState(false);
|
||||||
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
||||||
|
|
@ -74,8 +70,7 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
|
||||||
style={{
|
style={{
|
||||||
WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag',
|
WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor:
|
backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6))',
|
||||||
colorScheme === 'dark' ? 'var(--mantine-color-dark-6)' : 'var(--mantine-color-gray-1)',
|
|
||||||
borderBottom: '1px solid var(--mantine-color-default-border)',
|
borderBottom: '1px solid var(--mantine-color-default-border)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
height: TITLEBAR_HEIGHT,
|
height: TITLEBAR_HEIGHT,
|
||||||
|
|
@ -154,9 +149,7 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
colorScheme === 'dark'
|
'light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3))',
|
||||||
? 'var(--mantine-color-dark-3)'
|
|
||||||
: 'var(--mantine-color-gray-4)',
|
|
||||||
height: '1.25rem',
|
height: '1.25rem',
|
||||||
margin: '0 0.25rem',
|
margin: '0 0.25rem',
|
||||||
width: '0.1rem',
|
width: '0.1rem',
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export const UpdateAvailableModal = ({
|
||||||
closeOnEscape={!isDownloading && !isUpdating}
|
closeOnEscape={!isDownloading && !isUpdating}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Card withBorder radius="md" p="md" bd="2px solid orange">
|
<Card withBorder radius="md" p="md" bd="2px solid var(--mantine-color-orange-5)">
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group gap="md" align="center">
|
<Group gap="md" align="center">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,6 @@ export const UpdateButton = () => {
|
||||||
color={color}
|
color={color}
|
||||||
size={TITLEBAR_HEIGHT}
|
size={TITLEBAR_HEIGHT}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
tabIndex={-1}
|
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { Download, Trash2 } from 'lucide-react';
|
||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
import type { BackendInfo } from '@/types';
|
import type { BackendInfo } from '@/types';
|
||||||
import { isWindowsROCmBuild, pretifyBinName } from '@/utils/assets';
|
import { isWindowsROCmBuild, pretifyBinName } from '@/utils/assets';
|
||||||
|
|
||||||
|
|
@ -30,7 +29,6 @@ export const DownloadCard = ({
|
||||||
onRedownload,
|
onRedownload,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: DownloadCardProps) => {
|
}: DownloadCardProps) => {
|
||||||
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
|
|
||||||
const { downloading, downloadProgress } = useKoboldBackendsStore();
|
const { downloading, downloadProgress } = useKoboldBackendsStore();
|
||||||
|
|
||||||
const isLoading = downloading === backend.name;
|
const isLoading = downloading === backend.name;
|
||||||
|
|
@ -156,8 +154,10 @@ export const DownloadCard = ({
|
||||||
radius="sm"
|
radius="sm"
|
||||||
padding="sm"
|
padding="sm"
|
||||||
{...(backend.isCurrent && {
|
{...(backend.isCurrent && {
|
||||||
bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`,
|
style: {
|
||||||
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
|
border: 'light-dark(var(--mantine-color-brand-6), var(--mantine-color-brand-4))',
|
||||||
|
backgroundColor: 'light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6))',
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
|
|
@ -167,7 +167,7 @@ export const DownloadCard = ({
|
||||||
{pretifyBinName(backend.name)}
|
{pretifyBinName(backend.name)}
|
||||||
</Text>
|
</Text>
|
||||||
{backend.isCurrent && (
|
{backend.isCurrent && (
|
||||||
<Badge variant="light" color="blue" size="sm">
|
<Badge variant="light" color="brand" size="sm">
|
||||||
Current
|
Current
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
@ -217,8 +217,8 @@ export const DownloadCard = ({
|
||||||
|
|
||||||
{isLoading && currentProgress !== undefined && (
|
{isLoading && currentProgress !== undefined && (
|
||||||
<Stack gap="xs" mt="sm">
|
<Stack gap="xs" mt="sm">
|
||||||
<Progress value={currentProgress} color="blue" radius="xl" />
|
<Progress value={currentProgress} color="brand" radius="xl" />
|
||||||
<Text size="xs" c="dimmed" ta="center">
|
<Text size="xs" c="dimmed" ta="center" aria-live="polite">
|
||||||
{currentProgress === 100
|
{currentProgress === 100
|
||||||
? '100.0% complete'
|
? '100.0% complete'
|
||||||
: `${currentProgress.toFixed(1).padStart(5, ' ')}% complete`}
|
: `${currentProgress.toFixed(1).padStart(5, ' ')}% complete`}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ interface InfoTooltipProps {
|
||||||
|
|
||||||
export const InfoTooltip = ({ label, multiline = true, width = 300 }: InfoTooltipProps) => (
|
export const InfoTooltip = ({ label, multiline = true, width = 300 }: InfoTooltipProps) => (
|
||||||
<Tooltip label={label} multiline={multiline} w={width}>
|
<Tooltip label={label} multiline={multiline} w={width}>
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
<ActionIcon variant="subtle" size="sm" color="gray" aria-label="More information">
|
||||||
<Info size={14} />
|
<Info size={14} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
import { NOTEPAD_MIN_HEIGHT, NOTEPAD_MIN_WIDTH } from '@/constants/notepad';
|
import { NOTEPAD_MIN_HEIGHT, NOTEPAD_MIN_WIDTH } from '@/constants/notepad';
|
||||||
import { useNotepadStore } from '@/stores/notepad';
|
import { useNotepadStore } from '@/stores/notepad';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
|
|
||||||
import { CloseConfirmModal } from './CloseConfirmModal.tsx';
|
import { CloseConfirmModal } from './CloseConfirmModal.tsx';
|
||||||
import { NotepadEditor } from './Editor.tsx';
|
import { NotepadEditor } from './Editor.tsx';
|
||||||
|
|
@ -23,7 +22,6 @@ export const NotepadContainer = () => {
|
||||||
isVisible,
|
isVisible,
|
||||||
setVisible,
|
setVisible,
|
||||||
} = useNotepadStore();
|
} = useNotepadStore();
|
||||||
const { resolvedColorScheme } = usePreferencesStore();
|
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [resizeDirection, setResizeDirection] = useState<string | null>(null);
|
const [resizeDirection, setResizeDirection] = useState<string | null>(null);
|
||||||
|
|
@ -125,10 +123,7 @@ export const NotepadContainer = () => {
|
||||||
shadow="lg"
|
shadow="lg"
|
||||||
withBorder
|
withBorder
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
|
||||||
resolvedColorScheme === 'dark'
|
|
||||||
? 'var(--mantine-color-dark-6)'
|
|
||||||
: 'var(--mantine-color-white)',
|
|
||||||
bottom: 24,
|
bottom: 24,
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
height: position.height,
|
height: position.height,
|
||||||
|
|
@ -199,11 +194,8 @@ export const NotepadContainer = () => {
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderBottom: `1px solid ${
|
borderBottom:
|
||||||
resolvedColorScheme === 'dark'
|
'1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))',
|
||||||
? 'var(--mantine-color-dark-4)'
|
|
||||||
: 'var(--mantine-color-gray-3)'
|
|
||||||
}`,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
minHeight: 28,
|
minHeight: 28,
|
||||||
|
|
@ -220,13 +212,31 @@ export const NotepadContainer = () => {
|
||||||
paddingRight: 8,
|
paddingRight: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ActionIcon variant="subtle" size="xs" onClick={() => setVisible(false)}>
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
aria-label="Minimize notepad"
|
||||||
|
onClick={() => setVisible(false)}
|
||||||
|
>
|
||||||
<Minus size="1rem" />
|
<Minus size="1rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box style={{ flex: 1, position: 'relative' }}>
|
<Box
|
||||||
|
role="tabpanel"
|
||||||
|
id={
|
||||||
|
activeTab
|
||||||
|
? `notepad-panel-${activeTab.title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
aria-labelledby={
|
||||||
|
activeTab
|
||||||
|
? `notepad-tab-${activeTab.title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
style={{ flex: 1, position: 'relative' }}
|
||||||
|
>
|
||||||
{activeTab && <NotepadEditor key={activeTab.title} tab={activeTab} />}
|
{activeTab && <NotepadEditor key={activeTab.title} tab={activeTab} />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -20,24 +20,22 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
const { saveTabContent, showLineNumbers, setShowLineNumbers } = useNotepadStore();
|
const { saveTabContent, showLineNumbers, setShowLineNumbers } = useNotepadStore();
|
||||||
const { resolvedColorScheme } = usePreferencesStore();
|
const { resolvedColorScheme } = usePreferencesStore();
|
||||||
const [content, setContent] = useState(() => tab.content);
|
const [content, setContent] = useState(() => tab.content);
|
||||||
const [saveTimeout, setSaveTimeout] = useState<ReturnType<typeof setTimeout> | null>(null);
|
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||||||
|
|
||||||
const handleContentChange = useCallback(
|
const handleContentChange = useCallback(
|
||||||
(newContent: string) => {
|
(newContent: string) => {
|
||||||
setContent(newContent);
|
setContent(newContent);
|
||||||
|
|
||||||
if (saveTimeout) {
|
if (saveTimeoutRef.current) {
|
||||||
clearTimeout(saveTimeout);
|
clearTimeout(saveTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
saveTimeoutRef.current = setTimeout(() => {
|
||||||
void saveTabContent(tab.title, newContent);
|
void saveTabContent(tab.title, newContent);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
setSaveTimeout(timeout);
|
|
||||||
},
|
},
|
||||||
[tab.title, saveTabContent, saveTimeout],
|
[tab.title, saveTabContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEditorContextMenu = (e: MouseEvent) => {
|
const handleEditorContextMenu = (e: MouseEvent) => {
|
||||||
|
|
@ -56,11 +54,11 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
if (saveTimeout) {
|
if (saveTimeoutRef.current) {
|
||||||
clearTimeout(saveTimeout);
|
clearTimeout(saveTimeoutRef.current);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[saveTimeout],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ import { X } from 'lucide-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import type { DragEvent, KeyboardEvent, MouseEvent } from 'react';
|
import type { DragEvent, KeyboardEvent, MouseEvent } from 'react';
|
||||||
|
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
title: string;
|
title: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
|
@ -34,7 +32,6 @@ export const Tab = ({
|
||||||
showLineNumbers,
|
showLineNumbers,
|
||||||
setShowLineNumbers,
|
setShowLineNumbers,
|
||||||
}: TabProps) => {
|
}: TabProps) => {
|
||||||
const { resolvedColorScheme } = usePreferencesStore();
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editingTitle, setEditingTitle] = useState(title);
|
const [editingTitle, setEditingTitle] = useState(title);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
@ -97,28 +94,32 @@ export const Tab = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
id={`notepad-tab-${title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`}
|
||||||
|
role="tab"
|
||||||
|
aria-selected={isActive}
|
||||||
|
aria-controls={`notepad-panel-${title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
draggable={!isEditing}
|
draggable={!isEditing}
|
||||||
onClick={handleTabClick}
|
onClick={handleTabClick}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
|
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (!isEditing && (e.key === 'Enter' || e.key === ' ')) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleTabClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
onDragStart={(e) => onDragStart(e, index)}
|
onDragStart={(e) => onDragStart(e, index)}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDrop={(e) => onDrop(e, index)}
|
onDrop={(e) => onDrop(e, index)}
|
||||||
style={{
|
style={{
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: isActive
|
backgroundColor: isActive
|
||||||
? resolvedColorScheme === 'dark'
|
? 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-4))'
|
||||||
? 'var(--mantine-color-dark-4)'
|
|
||||||
: 'var(--mantine-color-gray-1)'
|
|
||||||
: isDragOver
|
: isDragOver
|
||||||
? resolvedColorScheme === 'dark'
|
? 'light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5))'
|
||||||
? 'var(--mantine-color-dark-5)'
|
|
||||||
: 'var(--mantine-color-gray-2)'
|
|
||||||
: 'transparent',
|
: 'transparent',
|
||||||
borderRight: `1px solid ${
|
borderRight:
|
||||||
resolvedColorScheme === 'dark'
|
'1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))',
|
||||||
? 'var(--mantine-color-dark-4)'
|
|
||||||
: 'var(--mantine-color-gray-3)'
|
|
||||||
}`,
|
|
||||||
cursor: isEditing ? 'default' : 'pointer',
|
cursor: isEditing ? 'default' : 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '0.25rem',
|
gap: '0.25rem',
|
||||||
|
|
@ -173,7 +174,7 @@ export const Tab = ({
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ActionIcon variant="subtle" size="xs" onClick={onClose}>
|
<ActionIcon variant="subtle" size="sm" aria-label="Close tab" onClick={onClose}>
|
||||||
<X size="0.625rem" />
|
<X size="0.625rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { ActionIcon, Box } from '@mantine/core';
|
import { ActionIcon, Box } from '@mantine/core';
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import type { DragEvent, MouseEvent } from 'react';
|
import type { DragEvent, KeyboardEvent, MouseEvent } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Tab } from '@/components/Notepad/Tab';
|
import { Tab } from '@/components/Notepad/Tab';
|
||||||
import { useNotepadStore } from '@/stores/notepad';
|
import { useNotepadStore } from '@/stores/notepad';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
|
|
||||||
interface NotepadTabsProps {
|
interface NotepadTabsProps {
|
||||||
onCreateNewTab: () => Promise<void>;
|
onCreateNewTab: () => Promise<void>;
|
||||||
|
|
@ -27,7 +26,6 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
||||||
showLineNumbers,
|
showLineNumbers,
|
||||||
setShowLineNumbers,
|
setShowLineNumbers,
|
||||||
} = useNotepadStore();
|
} = useNotepadStore();
|
||||||
const { resolvedColorScheme } = usePreferencesStore();
|
|
||||||
const [draggedTabIndex, setDraggedTabIndex] = useState<number | null>(null);
|
const [draggedTabIndex, setDraggedTabIndex] = useState<number | null>(null);
|
||||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
|
@ -53,6 +51,19 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
const currentIndex = tabs.findIndex((t) => t.title === activeTabId);
|
||||||
|
if (e.key === 'ArrowRight') {
|
||||||
|
e.preventDefault();
|
||||||
|
const next = (currentIndex + 1) % tabs.length;
|
||||||
|
setActiveTab(tabs[next].title);
|
||||||
|
} else if (e.key === 'ArrowLeft') {
|
||||||
|
e.preventDefault();
|
||||||
|
const prev = (currentIndex - 1 + tabs.length) % tabs.length;
|
||||||
|
setActiveTab(tabs[prev].title);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDragStart = (e: DragEvent, index: number) => {
|
const handleDragStart = (e: DragEvent, index: number) => {
|
||||||
setDraggedTabIndex(index);
|
setDraggedTabIndex(index);
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
|
@ -79,13 +90,13 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
role="tablist"
|
||||||
|
aria-label="Notepad tabs"
|
||||||
onContextMenu={handleTabBarContextMenu}
|
onContextMenu={handleTabBarContextMenu}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
style={{
|
style={{
|
||||||
borderBottom: `1px solid ${
|
borderBottom:
|
||||||
resolvedColorScheme === 'dark'
|
'1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))',
|
||||||
? 'var(--mantine-color-dark-4)'
|
|
||||||
: 'var(--mantine-color-gray-3)'
|
|
||||||
}`,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
minHeight: '2rem',
|
minHeight: '2rem',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
|
@ -116,7 +127,8 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
|
||||||
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="xs"
|
size="sm"
|
||||||
|
aria-label="New tab"
|
||||||
onClick={() => void onCreateNewTab()}
|
onClick={() => void onCreateNewTab()}
|
||||||
style={{
|
style={{
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ export const Switch = (props: SwitchProps) => (
|
||||||
<MantineSwitch
|
<MantineSwitch
|
||||||
{...props}
|
{...props}
|
||||||
styles={{
|
styles={{
|
||||||
thumb: { transition: 'none' },
|
thumb: { transition: 'left 100ms ease, background-color 100ms ease' },
|
||||||
track: { transition: 'none' },
|
track: { transition: 'background-color 100ms ease' },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => {
|
||||||
warningMessages[0].message
|
warningMessages[0].message
|
||||||
) : (
|
) : (
|
||||||
<List size="sm" spacing={4}>
|
<List size="sm" spacing={4}>
|
||||||
{warningMessages.map((warning, index) => (
|
{warningMessages.map((warning) => (
|
||||||
<List.Item key={index}>{warning.message}</List.Item>
|
<List.Item key={warning.message}>{warning.message}</List.Item>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
)
|
)
|
||||||
|
|
@ -48,8 +48,8 @@ export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => {
|
||||||
infoMessages[0].message
|
infoMessages[0].message
|
||||||
) : (
|
) : (
|
||||||
<List size="sm" spacing={4}>
|
<List size="sm" spacing={4}>
|
||||||
{infoMessages.map((info, index) => (
|
{infoMessages.map((info) => (
|
||||||
<List.Item key={index}>{info.message}</List.Item>
|
<List.Item key={info.message}>{info.message}</List.Item>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
)
|
)
|
||||||
|
|
@ -57,7 +57,7 @@ export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => {
|
||||||
multiline
|
multiline
|
||||||
maw={320}
|
maw={320}
|
||||||
>
|
>
|
||||||
<Info size={18} color="var(--mantine-color-blue-6)" strokeWidth={2} />
|
<Info size={18} color="var(--mantine-color-brand-5)" strokeWidth={2} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Card, Container, Loader, Stack, Text, Title } from '@mantine/core';
|
import { Container, Loader, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { DownloadCard } from '@/components/DownloadCard';
|
import { DownloadCard } from '@/components/DownloadCard';
|
||||||
|
|
@ -59,13 +59,12 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="sm" mt="md">
|
<Container size="sm" mt="md">
|
||||||
<Card withBorder radius="md" shadow="sm">
|
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={3}>Select a Backend</Title>
|
<Title order={3}>Select a Backend</Title>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Stack align="center" gap="md" py="xl">
|
<Stack align="center" gap="md" py="xl">
|
||||||
<Loader color="blue" />
|
<Loader color="brand" />
|
||||||
<Text c="dimmed">Preparing download options...</Text>
|
<Text c="dimmed">Preparing download options...</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -73,8 +72,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
{availableDownloads.length > 0 ? (
|
{availableDownloads.length > 0 ? (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{availableDownloads.map((download) => {
|
{availableDownloads.map((download) => {
|
||||||
const isDownloading =
|
const isDownloading = Boolean(downloading) && downloadingAsset === download.name;
|
||||||
Boolean(downloading) && downloadingAsset === download.name;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={download.name} ref={isDownloading ? downloadingItemRef : null}>
|
<div key={download.name} ref={isDownloading ? downloadingItemRef : null}>
|
||||||
|
|
@ -91,8 +89,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
size={formatDownloadSize(download.size, download.url)}
|
size={formatDownloadSize(download.size, download.url)}
|
||||||
description={getAssetDescription(download.name)}
|
description={getAssetDescription(download.name)}
|
||||||
disabled={
|
disabled={
|
||||||
importing ||
|
importing || (Boolean(downloading) && downloadingAsset !== download.name)
|
||||||
(Boolean(downloading) && downloadingAsset !== download.name)
|
|
||||||
}
|
}
|
||||||
onDownload={(e) => {
|
onDownload={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -118,7 +115,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { ChevronDown } from 'lucide-react';
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
|
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
|
||||||
|
|
||||||
export interface TerminalTabRef {
|
export interface TerminalTabRef {
|
||||||
|
|
@ -11,7 +10,6 @@ export interface TerminalTabRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||||
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
|
|
||||||
const [terminalContent, setTerminalContent] = useState('');
|
const [terminalContent, setTerminalContent] = useState('');
|
||||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||||
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
|
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
|
||||||
|
|
@ -87,9 +85,7 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
colorScheme === 'dark'
|
'light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-filled))',
|
||||||
? 'var(--mantine-color-dark-filled)'
|
|
||||||
: 'var(--mantine-color-gray-0)',
|
|
||||||
borderRadius: 'inherit',
|
borderRadius: 'inherit',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
|
@ -101,22 +97,14 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||||
ref={scrollAreaRef}
|
ref={scrollAreaRef}
|
||||||
viewportRef={viewportRef}
|
viewportRef={viewportRef}
|
||||||
onScrollPositionChange={handleScroll}
|
onScrollPositionChange={handleScroll}
|
||||||
style={{
|
style={{ flex: 1 }}
|
||||||
flex: 1,
|
|
||||||
fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace',
|
|
||||||
fontSize: '0.875em',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
}}
|
|
||||||
scrollbarSize={8}
|
scrollbarSize={8}
|
||||||
offsetScrollbars={false}
|
offsetScrollbars={false}
|
||||||
>
|
>
|
||||||
<Box p="md">
|
<Box p="md">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
color:
|
color: 'light-dark(var(--mantine-color-dark-filled), var(--mantine-color-gray-0))',
|
||||||
colorScheme === 'dark'
|
|
||||||
? 'var(--mantine-color-gray-0)'
|
|
||||||
: 'var(--mantine-color-dark-filled)',
|
|
||||||
fontFamily:
|
fontFamily:
|
||||||
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
fontSize: '0.875em',
|
fontSize: '0.875em',
|
||||||
|
|
@ -137,13 +125,13 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
|
||||||
{isUserScrolling && !shouldAutoScroll && (
|
{isUserScrolling && !shouldAutoScroll && (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="blue"
|
color="brand"
|
||||||
size="lg"
|
size="lg"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={scrollToBottom}
|
onClick={scrollToBottom}
|
||||||
style={{
|
style={{
|
||||||
bottom: '1.25rem',
|
bottom: '1.25rem',
|
||||||
boxShadow: '0 0.125rem 0.5rem rgba(0, 0, 0, 0.3)',
|
boxShadow: 'var(--gerbil-shadow-sm)',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: '1.25rem',
|
right: '1.25rem',
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ export const AdvancedTab = () => {
|
||||||
</Group>
|
</Group>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
{preLaunchCommands.map((command, index) => (
|
{preLaunchCommands.map((command, index) => (
|
||||||
<Group key={index} gap="xs">
|
<Group key={`cmd-${index}-${command}`} gap="xs">
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Enter a shell command"
|
placeholder="Enter a shell command"
|
||||||
value={command}
|
value={command}
|
||||||
|
|
@ -174,6 +174,7 @@ export const AdvancedTab = () => {
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="red"
|
color="red"
|
||||||
disabled={preLaunchCommands.length === 1}
|
disabled={preLaunchCommands.length === 1}
|
||||||
|
aria-label={`Remove command ${index + 1}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newCommands = preLaunchCommands.filter((_, i) => i !== index);
|
const newCommands = preLaunchCommands.filter((_, i) => i !== index);
|
||||||
setPreLaunchCommands(newCommands.length === 0 ? [''] : newCommands);
|
setPreLaunchCommands(newCommands.length === 0 ? [''] : newCommands);
|
||||||
|
|
|
||||||
|
|
@ -885,7 +885,11 @@ export const CommandLineArgumentsModal = ({
|
||||||
gap="xs"
|
gap="xs"
|
||||||
p="sm"
|
p="sm"
|
||||||
style={{
|
style={{
|
||||||
borderLeft: '3px solid var(--mantine-color-blue-4)',
|
background:
|
||||||
|
'light-dark(var(--mantine-color-brand-0), var(--mantine-color-dark-6))',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
border:
|
||||||
|
'1px solid light-dark(var(--mantine-color-brand-2), var(--mantine-color-dark-4))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group gap="xs" wrap="wrap" justify="space-between">
|
<Group gap="xs" wrap="wrap" justify="space-between">
|
||||||
|
|
@ -897,7 +901,7 @@ export const CommandLineArgumentsModal = ({
|
||||||
</Code>
|
</Code>
|
||||||
))}
|
))}
|
||||||
{arg.type && (
|
{arg.type && (
|
||||||
<Badge size="xs" variant="light" color="blue">
|
<Badge size="xs" variant="light" color="brand">
|
||||||
{arg.type}
|
{arg.type}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export const AccelerationSelectItem = ({
|
||||||
discreteDevices.length > 0 && (
|
discreteDevices.length > 0 && (
|
||||||
<Group gap={4}>
|
<Group gap={4}>
|
||||||
{discreteDevices.slice(0, 2).map((device, index) => (
|
{discreteDevices.slice(0, 2).map((device, index) => (
|
||||||
<Badge key={index} size="md" variant="light" color="blue">
|
<Badge key={index} size="md" variant="light" color="brand">
|
||||||
{renderDeviceName(device)}
|
{renderDeviceName(device)}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,10 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
img: ({ node: _, ...props }) => (
|
img: ({ node: _, alt, ...props }) => (
|
||||||
<img
|
<img
|
||||||
{...props}
|
{...props}
|
||||||
|
alt={alt ?? ''}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ export const ModelsTable = ({
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{model.paramSize && (
|
{model.paramSize && (
|
||||||
<Badge size="sm" variant="light" color="blue">
|
<Badge size="sm" variant="light" color="brand">
|
||||||
{model.paramSize}
|
{model.paramSize}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ export const HuggingFaceSearchModal = ({
|
||||||
<Stack gap="md" style={{ height: '100%' }}>
|
<Stack gap="md" style={{ height: '100%' }}>
|
||||||
{selectedModel ? (
|
{selectedModel ? (
|
||||||
<Group gap="xs" wrap="nowrap">
|
<Group gap="xs" wrap="nowrap">
|
||||||
<ActionIcon variant="subtle" onClick={handleBack}>
|
<ActionIcon variant="subtle" aria-label="Back to search results" onClick={handleBack}>
|
||||||
<ArrowLeft size={18} />
|
<ArrowLeft size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
|
@ -148,7 +148,11 @@ export const HuggingFaceSearchModal = ({
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Tooltip label="Open on Hugging Face">
|
<Tooltip label="Open on Hugging Face">
|
||||||
<ActionIcon variant="subtle" onClick={handleOpenExternal}>
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
aria-label="Open on Hugging Face"
|
||||||
|
onClick={handleOpenExternal}
|
||||||
|
>
|
||||||
<ExternalLink size={16} />
|
<ExternalLink size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,12 @@ export const ModelFileField = ({
|
||||||
</Button>
|
</Button>
|
||||||
{searchParams && (
|
{searchParams && (
|
||||||
<Tooltip label="Search Hugging Face">
|
<Tooltip label="Search Hugging Face">
|
||||||
<ActionIcon onClick={() => setSearchModalOpened(true)} variant="outline" size="lg">
|
<ActionIcon
|
||||||
|
onClick={() => setSearchModalOpened(true)}
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
aria-label="Search Hugging Face"
|
||||||
|
>
|
||||||
<Search size={16} />
|
<Search size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -156,9 +161,10 @@ export const ModelFileField = ({
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => void handleAnalyzeModel()}
|
onClick={() => void handleAnalyzeModel()}
|
||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="brand"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={validationState === 'neutral' || validationState === 'invalid'}
|
disabled={validationState === 'neutral' || validationState === 'invalid'}
|
||||||
|
aria-label="Analyze model"
|
||||||
>
|
>
|
||||||
<Info size={16} />
|
<Info size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
|
||||||
|
|
@ -425,7 +425,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
onClick={handleLaunchClick}
|
onClick={handleLaunchClick}
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="blue"
|
color="brand"
|
||||||
style={{
|
style={{
|
||||||
fontSize: '1em',
|
fontSize: '1em',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,5 @@
|
||||||
import iconUrl from '/icon.png';
|
import iconUrl from '/icon.png';
|
||||||
import {
|
import { Button, Container, Group, Image, List, Stack, Text, Title } from '@mantine/core';
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Container,
|
|
||||||
Group,
|
|
||||||
Image,
|
|
||||||
List,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
ThemeIcon,
|
|
||||||
Title,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { Check } from 'lucide-react';
|
|
||||||
|
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
|
|
||||||
|
|
@ -21,77 +9,51 @@ interface WelcomeScreenProps {
|
||||||
|
|
||||||
export const WelcomeScreen = ({ onGetStarted }: WelcomeScreenProps) => (
|
export const WelcomeScreen = ({ onGetStarted }: WelcomeScreenProps) => (
|
||||||
<Container size="md" mt="md">
|
<Container size="md" mt="md">
|
||||||
<Stack gap="xl">
|
<Stack gap="lg" maw={560}>
|
||||||
<Card withBorder radius="md" shadow="sm" p="xl">
|
<Stack gap="sm">
|
||||||
<Stack gap="lg" align="center">
|
<Group gap="md" align="center">
|
||||||
<Stack gap="md" align="center">
|
|
||||||
<Group gap="md" mr="xl" align="center">
|
|
||||||
<Image src={iconUrl} alt={PRODUCT_NAME} w={36} h={36} />
|
<Image src={iconUrl} alt={PRODUCT_NAME} w={36} h={36} />
|
||||||
<Title order={1} ta="center">
|
<Title order={1}>{PRODUCT_NAME}</Title>
|
||||||
{PRODUCT_NAME}
|
|
||||||
</Title>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="lg" c="dimmed" ta="center" maw={600}>
|
<Text size="lg" c="dimmed">
|
||||||
Run Large Language Models locally
|
Run Large Language Models locally on your hardware.
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack gap="lg" w="100%" maw={600}>
|
<List spacing="xs" size="sm">
|
||||||
<List
|
|
||||||
spacing="sm"
|
|
||||||
size="sm"
|
|
||||||
center
|
|
||||||
icon={
|
|
||||||
<ThemeIcon color="green" size={20} radius="xl">
|
|
||||||
<Check size={12} />
|
|
||||||
</ThemeIcon>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Text>
|
<Text fw={500} component="span">
|
||||||
<Text component="span" fw={500}>
|
Chat
|
||||||
Chat with AI models
|
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
- Have conversations, ask questions, get help with writing
|
— conversations, questions, writing help
|
||||||
</Text>
|
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Text>
|
<Text fw={500} component="span">
|
||||||
<Text component="span" fw={500}>
|
Image generation
|
||||||
Generate images
|
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
- Easily create artwork and illustrations with LLMs
|
— artwork and illustrations via LLMs
|
||||||
</Text>
|
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Text>
|
<Text fw={500} component="span">
|
||||||
<Text component="span" fw={500}>
|
Private
|
||||||
Complete privacy
|
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
- Everything runs on your computer, no data sent to servers
|
— everything runs locally, no data leaves the machine
|
||||||
</Text>
|
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Text>
|
<Text fw={500} component="span">
|
||||||
<Text component="span" fw={500}>
|
Accelerated
|
||||||
Hardware acceleration
|
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
- Supports CUDA, ROCm and Vulkan backends
|
— CUDA, ROCm, and Vulkan backends supported
|
||||||
</Text>
|
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Text size="xs" c="dimmed" ta="center" mt="sm">
|
<Text size="xs" c="dimmed">
|
||||||
Hardware acceleration requires appropriate drivers to be manually installed (CUDA for
|
Hardware acceleration requires drivers installed separately (CUDA for NVIDIA, ROCm for AMD).
|
||||||
NVIDIA GPUs, ROCm for AMD GPUs, etc.)
|
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Button size="lg" mt="lg" onClick={onGetStarted}>
|
<Button size="md" onClick={onGetStarted} style={{ alignSelf: 'flex-start' }}>
|
||||||
Get Started
|
Get Started
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export const AboutTab = () => {
|
||||||
<Text size="xl" fw={600}>
|
<Text size="xl" fw={600}>
|
||||||
{PRODUCT_NAME}
|
{PRODUCT_NAME}
|
||||||
</Text>
|
</Text>
|
||||||
<Badge variant="light" color="blue" size="lg" style={{ textTransform: 'none' }}>
|
<Badge variant="light" color="brand" size="lg" style={{ textTransform: 'none' }}>
|
||||||
v{versionInfo.appVersion}
|
v{versionInfo.appVersion}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
@ -84,17 +84,17 @@ export const AboutTab = () => {
|
||||||
</Group>
|
</Group>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card withBorder radius="md" p="md">
|
<Stack gap="xs">
|
||||||
<Text size="lg" fw={500} mb="md">
|
<Text size="sm" fw={500} c="dimmed">
|
||||||
About {PRODUCT_NAME}
|
About {PRODUCT_NAME}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" mb="md">
|
<Text size="sm" c="dimmed">
|
||||||
{PRODUCT_NAME} is a user-friendly desktop application that makes it easy to run large
|
{PRODUCT_NAME} is a user-friendly desktop application that makes it easy to run large
|
||||||
language models locally on your machine. Whether you're looking to chat with AI
|
language models locally on your machine. Whether you're looking to chat with AI
|
||||||
models, generate images, or explore different interfaces like SillyTavern and Open WebUI,{' '}
|
models, generate images, or explore different interfaces like SillyTavern and Open WebUI,{' '}
|
||||||
{PRODUCT_NAME} provides a streamlined experience for local AI.
|
{PRODUCT_NAME} provides a streamlined experience for local AI.
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Group, rem, SegmentedControl, Slider, Stack, Text, TextInput } from '@m
|
||||||
import type { MantineColorScheme } from '@mantine/core';
|
import type { MantineColorScheme } from '@mantine/core';
|
||||||
import { Monitor, Moon, Sun } from 'lucide-react';
|
import { Monitor, Moon, Sun } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector';
|
import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector';
|
||||||
import { ZOOM } from '@/constants';
|
import { ZOOM } from '@/constants';
|
||||||
|
|
@ -13,13 +14,7 @@ interface AppearanceTabProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProps) => {
|
export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProps) => {
|
||||||
const {
|
const { rawColorScheme, setColorScheme: setStoreColorScheme } = usePreferencesStore();
|
||||||
rawColorScheme,
|
|
||||||
resolvedColorScheme,
|
|
||||||
setColorScheme: setStoreColorScheme,
|
|
||||||
} = usePreferencesStore();
|
|
||||||
const isDark = resolvedColorScheme === 'dark';
|
|
||||||
|
|
||||||
const [zoomLevel, setZoomLevel] = useState(ZOOM.DEFAULT_LEVEL as number);
|
const [zoomLevel, setZoomLevel] = useState(ZOOM.DEFAULT_LEVEL as number);
|
||||||
const [zoomPercentage, setZoomPercentage] = useState(ZOOM.DEFAULT_PERCENTAGE.toString());
|
const [zoomPercentage, setZoomPercentage] = useState(ZOOM.DEFAULT_PERCENTAGE.toString());
|
||||||
|
|
||||||
|
|
@ -74,15 +69,16 @@ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProp
|
||||||
fullWidth
|
fullWidth
|
||||||
value={rawColorScheme}
|
value={rawColorScheme}
|
||||||
onChange={handleColorSchemeChange}
|
onChange={handleColorSchemeChange}
|
||||||
styles={(theme) => ({
|
style={
|
||||||
indicator: {
|
{
|
||||||
backgroundColor: isDark ? theme.colors.dark[5] : theme.colors.gray[2],
|
'--sc-indicator-color':
|
||||||
border: `1px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
|
'light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5))',
|
||||||
},
|
'--sc-indicator-border':
|
||||||
root: {
|
'light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))',
|
||||||
border: `0.5px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
|
border:
|
||||||
},
|
'0.5px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))',
|
||||||
})}
|
} as CSSProperties
|
||||||
|
}
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
label: (
|
label: (
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@ export const BackendsTab = () => {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
size="sm"
|
size="sm"
|
||||||
c="blue"
|
c="brand"
|
||||||
>
|
>
|
||||||
<Group gap={4} align="center">
|
<Group gap={4} align="center">
|
||||||
<span>Release notes</span>
|
<span>Release notes</span>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
Wrench,
|
Wrench,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Modal } from '@/components/Modal';
|
import { Modal } from '@/components/Modal';
|
||||||
import { AboutTab } from '@/components/settings/AboutTab';
|
import { AboutTab } from '@/components/settings/AboutTab';
|
||||||
|
|
@ -38,23 +38,6 @@ export const SettingsModal = ({
|
||||||
|
|
||||||
const effectiveActiveTab = !showBackendsTab && activeTab === 'backends' ? 'general' : activeTab;
|
const effectiveActiveTab = !showBackendsTab && activeTab === 'backends' ? 'general' : activeTab;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (opened) {
|
|
||||||
const originalOverflow = document.body.style.overflow;
|
|
||||||
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
||||||
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = originalOverflow;
|
|
||||||
document.body.style.paddingRight = '';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}, [opened]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,32 @@
|
||||||
@import '@mantine/core/styles.css';
|
@import '@mantine/core/styles.css';
|
||||||
@import '@fontsource/inter/latin-400.css';
|
@import '@fontsource/geist/latin-400.css';
|
||||||
@import '@fontsource/inter/latin-500.css';
|
@import '@fontsource/geist/latin-500.css';
|
||||||
@import '@fontsource/inter/latin-600.css';
|
@import '@fontsource/geist/latin-600.css';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--gerbil-window-border: light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.08));
|
||||||
|
--gerbil-scrollbar-thumb: rgba(134, 142, 150, 0.5);
|
||||||
|
--gerbil-scrollbar-thumb-hover: rgba(134, 142, 150, 0.8);
|
||||||
|
--gerbil-scrollbar-thumb-active: rgba(73, 80, 87, 0.9);
|
||||||
|
--gerbil-shadow-sm: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mantine-color-scheme='dark'] {
|
||||||
|
--gerbil-scrollbar-thumb: rgba(173, 181, 189, 0.5);
|
||||||
|
--gerbil-scrollbar-thumb-hover: rgba(173, 181, 189, 0.8);
|
||||||
|
--gerbil-scrollbar-thumb-active: rgba(206, 212, 218, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
border-left: 1px solid light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.08));
|
border-left: 1px solid var(--gerbil-window-border);
|
||||||
border-right: 1px solid light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.08));
|
border-right: 1px solid var(--gerbil-window-border);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-mantine-color-scheme='light'] .mantine-Select-option:hover {
|
[data-mantine-color-scheme='light'] .mantine-Select-option:hover {
|
||||||
background-color: #e9ecef;
|
background-color: var(--mantine-color-gray-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbars */
|
/* Custom scrollbars */
|
||||||
|
|
@ -27,30 +41,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: rgba(134, 142, 150, 0.5);
|
background: var(--gerbil-scrollbar-thumb);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
transition: all 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(134, 142, 150, 0.8);
|
background: var(--gerbil-scrollbar-thumb-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:active {
|
::-webkit-scrollbar-thumb:active {
|
||||||
background: rgba(73, 80, 87, 0.9);
|
background: var(--gerbil-scrollbar-thumb-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode scrollbar support */
|
/* Dark mode scrollbar support */
|
||||||
[data-mantine-color-scheme='dark'] ::-webkit-scrollbar-thumb {
|
[data-mantine-color-scheme='dark'] ::-webkit-scrollbar-thumb {
|
||||||
background: rgba(173, 181, 189, 0.5);
|
background: var(--gerbil-scrollbar-thumb);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-mantine-color-scheme='dark'] ::-webkit-scrollbar-thumb:hover {
|
[data-mantine-color-scheme='dark'] ::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(173, 181, 189, 0.8);
|
background: var(--gerbil-scrollbar-thumb-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-mantine-color-scheme='dark'] ::-webkit-scrollbar-thumb:active {
|
[data-mantine-color-scheme='dark'] ::-webkit-scrollbar-thumb:active {
|
||||||
background: rgba(206, 212, 218, 0.9);
|
background: var(--gerbil-scrollbar-thumb-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TitleBar gerbil icon animations */
|
/* TitleBar gerbil icon animations */
|
||||||
|
|
|
||||||
24
src/theme.ts
24
src/theme.ts
|
|
@ -3,7 +3,21 @@ import type { CSSVariablesResolver } from '@mantine/core';
|
||||||
|
|
||||||
export const theme = createTheme({
|
export const theme = createTheme({
|
||||||
black: '#101113',
|
black: '#101113',
|
||||||
|
primaryColor: 'brand',
|
||||||
colors: {
|
colors: {
|
||||||
|
// Steel-blue: technical, enthusiast-grade — vibrant but cooler than Mantine default
|
||||||
|
brand: [
|
||||||
|
'#eef5ff', // 0 — faint wash
|
||||||
|
'#d8eaff', // 1
|
||||||
|
'#b0d0ff', // 2
|
||||||
|
'#7aaff7', // 3
|
||||||
|
'#4890f0', // 4 — dark mode accent
|
||||||
|
'#2b72e0', // 5
|
||||||
|
'#1e5ec8', // 6 — light primary
|
||||||
|
'#184eb0', // 7
|
||||||
|
'#113d88', // 8 — dark primary
|
||||||
|
'#0c2d64', // 9 — deep
|
||||||
|
],
|
||||||
gray: [
|
gray: [
|
||||||
'#f8f9fa',
|
'#f8f9fa',
|
||||||
'#f1f3f4',
|
'#f1f3f4',
|
||||||
|
|
@ -52,9 +66,9 @@ export const theme = createTheme({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontFamily: 'Inter, sans-serif',
|
fontFamily: 'Geist, sans-serif',
|
||||||
headings: {
|
headings: {
|
||||||
fontFamily: 'Inter, sans-serif',
|
fontFamily: 'Geist, sans-serif',
|
||||||
},
|
},
|
||||||
white: '#fafafa',
|
white: '#fafafa',
|
||||||
});
|
});
|
||||||
|
|
@ -65,14 +79,16 @@ export const cssVariablesResolver: CSSVariablesResolver = (t) => {
|
||||||
variables: { ...v8.variables },
|
variables: { ...v8.variables },
|
||||||
dark: {
|
dark: {
|
||||||
...v8.dark,
|
...v8.dark,
|
||||||
'--mantine-color-body': '#0f0f0f',
|
'--mantine-color-body': 'oklch(12% 0.008 240)',
|
||||||
'--mantine-color-default-border': '#2a2a2a',
|
'--mantine-color-default-border': 'oklch(22% 0.008 240)',
|
||||||
|
'--gerbil-link-color': 'var(--mantine-color-brand-4)',
|
||||||
},
|
},
|
||||||
light: {
|
light: {
|
||||||
...v8.light,
|
...v8.light,
|
||||||
'--mantine-color-body': '#fafafa',
|
'--mantine-color-body': '#fafafa',
|
||||||
'--mantine-color-white': '#fafafa',
|
'--mantine-color-white': '#fafafa',
|
||||||
'--mantine-color-default-border': '#dee2e6',
|
'--mantine-color-default-border': '#dee2e6',
|
||||||
|
'--gerbil-link-color': 'var(--mantine-color-brand-5)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const linkifyText = (text: string) =>
|
||||||
const cleanUrl = url.replace(/[.,;:!?]+$/, '');
|
const cleanUrl = url.replace(/[.,;:!?]+$/, '');
|
||||||
const trailingPunctuation = url.slice(cleanUrl.length);
|
const trailingPunctuation = url.slice(cleanUrl.length);
|
||||||
|
|
||||||
return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #339af0; text-decoration: underline; cursor: pointer;">${cleanUrl}</a>${trailingPunctuation}`;
|
return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: var(--gerbil-link-color, #4f8cc5); text-decoration: underline; cursor: pointer;">${cleanUrl}</a>${trailingPunctuation}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const escapeHtmlExceptLinks = (text: string) => {
|
const escapeHtmlExceptLinks = (text: string) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue