minimize icon for the notepad "close", fix notepad reisizing over iframes

This commit is contained in:
Egor 2025-09-24 09:45:15 -07:00
parent f390fc5bd4
commit 99acc22b78
5 changed files with 217 additions and 192 deletions

View file

@ -1,7 +1,7 @@
{
"name": "gerbil",
"productName": "Gerbil",
"version": "1.5.1",
"version": "1.5.2",
"description": "Run Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",

View file

@ -60,7 +60,7 @@ export const UpdateAvailableModal = ({
opened={opened}
onClose={onClose}
size="sm"
title="A newer version is available"
title="An update is available"
closeOnEscape={!isDownloading && !isUpdating}
>
<Stack gap="md">

View file

@ -1,6 +1,6 @@
import { useEffect, useRef, useState, type MouseEvent } from 'react';
import { Box, Paper, ActionIcon } from '@mantine/core';
import { X } from 'lucide-react';
import { Minus } from 'lucide-react';
import { useNotepadStore } from '@/stores/notepad';
import { usePreferencesStore } from '@/stores/preferences';
import { NOTEPAD_MIN_WIDTH, NOTEPAD_MIN_HEIGHT } from '@/constants/notepad';
@ -168,6 +168,27 @@ export const NotepadContainer = () => {
onMouseDown={handleResizeStart('top-right')}
/>
{resizeDirection && (
<Box
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: 9999,
backgroundColor: 'transparent',
cursor:
resizeDirection.includes('right') &&
resizeDirection.includes('top')
? 'ne-resize'
: resizeDirection.includes('right')
? 'ew-resize'
: 'ns-resize',
}}
/>
)}
<Box h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
<Box
style={{
@ -200,9 +221,8 @@ export const NotepadContainer = () => {
variant="subtle"
size="xs"
onClick={() => setVisible(false)}
color="red"
>
<X size={16} />
<Minus size="1rem" />
</ActionIcon>
</Box>
</Box>

View file

@ -0,0 +1,186 @@
import {
type MouseEvent,
type DragEvent,
type KeyboardEvent,
useState,
useRef,
useEffect,
} from 'react';
import { Box, ActionIcon, Text, TextInput } from '@mantine/core';
import { X } from 'lucide-react';
import { usePreferencesStore } from '@/stores/preferences';
interface TabProps {
title: string;
index: number;
isActive: boolean;
onSelect: () => void;
onClose: (e: MouseEvent) => void;
onDragStart: (e: DragEvent, index: number) => void;
onDragOver: (e: DragEvent) => void;
onDrop: (e: DragEvent, index: number) => void;
onRename: (newTitle: string) => void;
isDragOver: boolean;
showLineNumbers: boolean;
setShowLineNumbers: (show: boolean) => void;
}
export const Tab = ({
index,
isActive,
title,
onSelect,
onClose,
onDragStart,
onDragOver,
onDrop,
onRename,
isDragOver,
showLineNumbers,
setShowLineNumbers,
}: TabProps) => {
const { resolvedColorScheme } = usePreferencesStore();
const [isEditing, setIsEditing] = useState(false);
const [editingTitle, setEditingTitle] = useState(title);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditing]);
const handleTitleClick = (e: MouseEvent) => {
e.stopPropagation();
if (!isActive) {
onSelect();
}
};
const handleTitleDoubleClick = (e: MouseEvent) => {
e.stopPropagation();
if (isActive) {
setIsEditing(true);
setEditingTitle(title);
}
};
const handleInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSaveTitle();
} else if (e.key === 'Escape') {
setIsEditing(false);
setEditingTitle(title);
}
};
const handleInputBlur = () => {
handleSaveTitle();
};
const handleSaveTitle = () => {
const trimmedTitle = editingTitle.trim();
if (trimmedTitle && trimmedTitle !== title) {
onRename(trimmedTitle);
}
setIsEditing(false);
};
const handleTabClick = () => {
if (!isEditing) {
onSelect();
}
};
const handleMouseDown = (e: MouseEvent) => {
if (e.button === 1) {
e.preventDefault();
onClose(e);
}
};
return (
<Box
draggable={!isEditing}
onClick={handleTabClick}
onMouseDown={handleMouseDown}
onDragStart={(e) => onDragStart(e, index)}
onDragOver={onDragOver}
onDrop={(e) => onDrop(e, index)}
style={{
padding: '0.375rem 0.5rem',
backgroundColor: isActive
? resolvedColorScheme === 'dark'
? 'var(--mantine-color-dark-4)'
: 'var(--mantine-color-gray-1)'
: isDragOver
? resolvedColorScheme === 'dark'
? 'var(--mantine-color-dark-5)'
: 'var(--mantine-color-gray-2)'
: 'transparent',
borderRight: `1px solid ${
resolvedColorScheme === 'dark'
? 'var(--mantine-color-dark-4)'
: 'var(--mantine-color-gray-3)'
}`,
cursor: isEditing ? 'default' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
minWidth: 0,
maxWidth: '7.5rem',
opacity: isDragOver ? 0.5 : 1,
}}
>
{isEditing ? (
<TextInput
ref={inputRef}
value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)}
onKeyDown={handleInputKeyDown}
onBlur={handleInputBlur}
size="xs"
variant="unstyled"
style={{
flex: 1,
minWidth: 0,
}}
styles={{
input: {
fontSize: 'var(--mantine-font-size-xs)',
padding: 0,
minHeight: 'auto',
height: 'auto',
lineHeight: 1,
},
}}
/>
) : (
<Text
size="xs"
title={title}
onClick={handleTitleClick}
onDoubleClick={handleTitleDoubleClick}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
setShowLineNumbers(!showLineNumbers);
}}
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
}}
>
{title}
</Text>
)}
<ActionIcon variant="subtle" size="xs" onClick={onClose}>
<X size="0.625rem" />
</ActionIcon>
</Box>
);
};

View file

@ -1,13 +1,7 @@
import {
type MouseEvent,
type DragEvent,
type KeyboardEvent,
useState,
useRef,
useEffect,
} from 'react';
import { Box, ActionIcon, Text, TextInput } from '@mantine/core';
import { X, Plus } from 'lucide-react';
import { type MouseEvent, type DragEvent, useState } from 'react';
import { Box, ActionIcon } from '@mantine/core';
import { Plus } from 'lucide-react';
import { Tab } from '@/components/Notepad/Tab';
import { useNotepadStore } from '@/stores/notepad';
import { usePreferencesStore } from '@/stores/preferences';
@ -16,181 +10,6 @@ interface NotepadTabsProps {
onCloseTab: (title: string) => void;
}
interface TabProps {
title: string;
index: number;
isActive: boolean;
onSelect: () => void;
onClose: (e: MouseEvent) => void;
onDragStart: (e: DragEvent, index: number) => void;
onDragOver: (e: DragEvent) => void;
onDrop: (e: DragEvent, index: number) => void;
onRename: (newTitle: string) => void;
isDragOver: boolean;
showLineNumbers: boolean;
setShowLineNumbers: (show: boolean) => void;
}
const Tab = ({
index,
isActive,
title,
onSelect,
onClose,
onDragStart,
onDragOver,
onDrop,
onRename,
isDragOver,
showLineNumbers,
setShowLineNumbers,
}: TabProps) => {
const { resolvedColorScheme } = usePreferencesStore();
const [isEditing, setIsEditing] = useState(false);
const [editingTitle, setEditingTitle] = useState(title);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditing]);
const handleTitleClick = (e: MouseEvent) => {
e.stopPropagation();
if (!isActive) {
onSelect();
}
};
const handleTitleDoubleClick = (e: MouseEvent) => {
e.stopPropagation();
if (isActive) {
setIsEditing(true);
setEditingTitle(title);
}
};
const handleInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSaveTitle();
} else if (e.key === 'Escape') {
setIsEditing(false);
setEditingTitle(title);
}
};
const handleInputBlur = () => {
handleSaveTitle();
};
const handleSaveTitle = () => {
const trimmedTitle = editingTitle.trim();
if (trimmedTitle && trimmedTitle !== title) {
onRename(trimmedTitle);
}
setIsEditing(false);
};
const handleTabClick = () => {
if (!isEditing) {
onSelect();
}
};
const handleMouseDown = (e: MouseEvent) => {
if (e.button === 1) {
e.preventDefault();
onClose(e);
}
};
return (
<Box
draggable={!isEditing}
onClick={handleTabClick}
onMouseDown={handleMouseDown}
onDragStart={(e) => onDragStart(e, index)}
onDragOver={onDragOver}
onDrop={(e) => onDrop(e, index)}
style={{
padding: '6px 8px',
backgroundColor: isActive
? resolvedColorScheme === 'dark'
? 'var(--mantine-color-dark-4)'
: 'var(--mantine-color-gray-1)'
: isDragOver
? resolvedColorScheme === 'dark'
? 'var(--mantine-color-dark-5)'
: 'var(--mantine-color-gray-2)'
: 'transparent',
borderRight: `1px solid ${
resolvedColorScheme === 'dark'
? 'var(--mantine-color-dark-4)'
: 'var(--mantine-color-gray-3)'
}`,
cursor: isEditing ? 'default' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '4px',
minWidth: 0,
maxWidth: 120,
opacity: isDragOver ? 0.5 : 1,
}}
>
{isEditing ? (
<TextInput
ref={inputRef}
value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)}
onKeyDown={handleInputKeyDown}
onBlur={handleInputBlur}
size="xs"
variant="unstyled"
style={{
flex: 1,
minWidth: 0,
}}
styles={{
input: {
fontSize: 'var(--mantine-font-size-xs)',
padding: 0,
minHeight: 'auto',
height: 'auto',
lineHeight: 1,
},
}}
/>
) : (
<Text
size="xs"
title={title}
onClick={handleTitleClick}
onDoubleClick={handleTitleDoubleClick}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
setShowLineNumbers(!showLineNumbers);
}}
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
}}
>
{title}
</Text>
)}
<ActionIcon variant="subtle" size="xs" onClick={onClose}>
<X size={10} />
</ActionIcon>
</Box>
);
};
export const NotepadTabs = ({
onCreateNewTab,
onCloseTab,
@ -270,7 +89,7 @@ export const NotepadTabs = ({
}`,
display: 'flex',
overflow: 'hidden',
minHeight: 32,
minHeight: '2rem',
}}
>
{tabs.map((tab, index) => (
@ -305,7 +124,7 @@ export const NotepadTabs = ({
alignSelf: 'center',
}}
>
<Plus size={12} />
<Plus size="0.75rem" />
</ActionIcon>
</Box>
);