Stirling 2.0 (#3928)

# Description of Changes

<!--

File context for managing files between tools and views
Optimisation for large files
Updated Split to work with new file system and match Matts stepped
design closer

-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
This commit is contained in:
Reece Browne
2025-07-16 17:53:50 +01:00
committed by GitHub
parent 584e2ecee7
commit 922bbc9076
66 changed files with 8728 additions and 2519 deletions

View File

@@ -0,0 +1,168 @@
import React, { useState } from "react";
import { Box, Flex, Group, Text, Button, TextInput, Select, Badge } from "@mantine/core";
import { useTranslation } from "react-i18next";
import SearchIcon from "@mui/icons-material/Search";
import SortIcon from "@mui/icons-material/Sort";
import FileCard from "../fileManagement/FileCard";
import { FileWithUrl } from "../../types/file";
interface FileGridProps {
files: FileWithUrl[];
onRemove?: (index: number) => void;
onDoubleClick?: (file: FileWithUrl) => void;
onView?: (file: FileWithUrl) => void;
onEdit?: (file: FileWithUrl) => void;
onSelect?: (fileId: string) => void;
selectedFiles?: string[];
showSearch?: boolean;
showSort?: boolean;
maxDisplay?: number; // If set, shows only this many files with "Show All" option
onShowAll?: () => void;
showingAll?: boolean;
onDeleteAll?: () => void;
}
type SortOption = 'date' | 'name' | 'size';
const FileGrid = ({
files,
onRemove,
onDoubleClick,
onView,
onEdit,
onSelect,
selectedFiles = [],
showSearch = false,
showSort = false,
maxDisplay,
onShowAll,
showingAll = false,
onDeleteAll
}: FileGridProps) => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState("");
const [sortBy, setSortBy] = useState<SortOption>('date');
// Filter files based on search term
const filteredFiles = files.filter(file =>
file.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// Sort files
const sortedFiles = [...filteredFiles].sort((a, b) => {
switch (sortBy) {
case 'date':
return (b.lastModified || 0) - (a.lastModified || 0);
case 'name':
return a.name.localeCompare(b.name);
case 'size':
return (b.size || 0) - (a.size || 0);
default:
return 0;
}
});
// Apply max display limit if specified
const displayFiles = maxDisplay && !showingAll
? sortedFiles.slice(0, maxDisplay)
: sortedFiles;
const hasMoreFiles = maxDisplay && !showingAll && sortedFiles.length > maxDisplay;
return (
<Box >
{/* Search and Sort Controls */}
{(showSearch || showSort || onDeleteAll) && (
<Group mb="md" justify="space-between" wrap="wrap" gap="sm">
<Group gap="sm">
{showSearch && (
<TextInput
placeholder={t("fileManager.searchFiles", "Search files...")}
leftSection={<SearchIcon size={16} />}
value={searchTerm}
onChange={(e) => setSearchTerm(e.currentTarget.value)}
style={{ flexGrow: 1, maxWidth: 300, minWidth: 200 }}
/>
)}
{showSort && (
<Select
data={[
{ value: 'date', label: t("fileManager.sortByDate", "Sort by Date") },
{ value: 'name', label: t("fileManager.sortByName", "Sort by Name") },
{ value: 'size', label: t("fileManager.sortBySize", "Sort by Size") }
]}
value={sortBy}
onChange={(value) => setSortBy(value as SortOption)}
leftSection={<SortIcon size={16} />}
style={{ minWidth: 150 }}
/>
)}
</Group>
{onDeleteAll && (
<Button
color="red"
size="sm"
onClick={onDeleteAll}
>
{t("fileManager.deleteAll", "Delete All")}
</Button>
)}
</Group>
)}
{/* File Grid */}
<Flex
direction="row"
wrap="wrap"
gap="md"
h="30rem"
style={{ overflowY: "auto", width: "100%" }}
>
{displayFiles.map((file, idx) => {
const fileId = file.id || file.name;
const originalIdx = files.findIndex(f => (f.id || f.name) === fileId);
return (
<FileCard
key={fileId + idx}
file={file}
onRemove={onRemove ? () => onRemove(originalIdx) : undefined}
onDoubleClick={onDoubleClick ? () => onDoubleClick(file) : undefined}
onView={onView ? () => onView(file) : undefined}
onEdit={onEdit ? () => onEdit(file) : undefined}
isSelected={selectedFiles.includes(fileId)}
onSelect={onSelect ? () => onSelect(fileId) : undefined}
/>
);
})}
</Flex>
{/* Show All Button */}
{hasMoreFiles && onShowAll && (
<Group justify="center" mt="md">
<Button
variant="light"
onClick={onShowAll}
>
{t("fileManager.showAll", "Show All")} ({sortedFiles.length} files)
</Button>
</Group>
)}
{/* Empty State */}
{displayFiles.length === 0 && (
<Box style={{ textAlign: 'center', padding: '2rem' }}>
<Text c="dimmed">
{searchTerm
? t("fileManager.noFilesFound", "No files found matching your search")
: t("fileManager.noFiles", "No files available")
}
</Text>
</Box>
)}
</Box>
);
};
export default FileGrid;

View File

@@ -1,9 +1,13 @@
import React, { useState, useCallback, useRef } from 'react';
import { Stack, Button, Text, Center } from '@mantine/core';
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Stack, Button, Text, Center, Box, Divider } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import { useTranslation } from 'react-i18next';
import FilePickerModal from './FilePickerModal';
import { fileStorage } from '../../services/fileStorage';
import { FileWithUrl } from '../../types/file';
import FileGrid from './FileGrid';
import MultiSelectControls from './MultiSelectControls';
import { useFileManager } from '../../hooks/useFileManager';
interface FileUploadSelectorProps {
// Appearance
@@ -20,6 +24,10 @@ interface FileUploadSelectorProps {
// Loading state
loading?: boolean;
disabled?: boolean;
// Recent files
showRecentFiles?: boolean;
maxRecentFiles?: number;
}
const FileUploadSelector = ({
@@ -29,50 +37,94 @@ const FileUploadSelector = ({
sharedFiles = [],
onFileSelect,
onFilesSelect,
accept = ["application/pdf"],
accept = ["application/pdf", "application/zip", "application/x-zip-compressed"],
loading = false,
disabled = false,
showRecentFiles = true,
maxRecentFiles = 8,
}: FileUploadSelectorProps) => {
const { t } = useTranslation();
const [showFilePickerModal, setShowFilePickerModal] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileUpload = useCallback((uploadedFiles: File[]) => {
const [recentFiles, setRecentFiles] = useState<FileWithUrl[]>([]);
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, createFileSelectionHandlers } = useFileManager();
const refreshRecentFiles = useCallback(async () => {
const files = await loadRecentFiles();
setRecentFiles(files);
}, [loadRecentFiles]);
const handleNewFileUpload = useCallback(async (uploadedFiles: File[]) => {
if (uploadedFiles.length === 0) return;
if (showRecentFiles) {
try {
for (const file of uploadedFiles) {
await storeFile(file);
}
refreshRecentFiles();
} catch (error) {
console.error('Failed to save files to recent:', error);
}
}
if (onFilesSelect) {
onFilesSelect(uploadedFiles);
} else if (onFileSelect) {
onFileSelect(uploadedFiles[0]);
}
}, [onFileSelect, onFilesSelect]);
}, [onFileSelect, onFilesSelect, showRecentFiles, storeFile, refreshRecentFiles]);
const handleFileInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files && files.length > 0) {
const fileArray = Array.from(files);
console.log('File input change:', fileArray.length, 'files');
handleFileUpload(fileArray);
handleNewFileUpload(fileArray);
}
// Reset input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}, [handleFileUpload]);
}, [handleNewFileUpload]);
const openFileDialog = useCallback(() => {
fileInputRef.current?.click();
}, []);
const handleStorageSelection = useCallback((selectedFiles: File[]) => {
if (selectedFiles.length === 0) return;
if (onFilesSelect) {
onFilesSelect(selectedFiles);
} else if (onFileSelect) {
onFileSelect(selectedFiles[0]);
const handleRecentFileSelection = useCallback(async (file: FileWithUrl) => {
try {
const fileObj = await convertToFile(file);
if (onFilesSelect) {
onFilesSelect([fileObj]);
} else if (onFileSelect) {
onFileSelect(fileObj);
}
} catch (error) {
console.error('Failed to load file from recent:', error);
}
}, [onFileSelect, onFilesSelect]);
}, [onFileSelect, onFilesSelect, convertToFile]);
const selectionHandlers = createFileSelectionHandlers(selectedFiles, setSelectedFiles);
const handleSelectedRecentFiles = useCallback(async () => {
if (onFilesSelect) {
await selectionHandlers.selectMultipleFiles(recentFiles, onFilesSelect);
}
}, [recentFiles, onFilesSelect, selectionHandlers]);
const handleRemoveFileByIndex = useCallback(async (index: number) => {
await handleRemoveFile(index, recentFiles, setRecentFiles);
const file = recentFiles[index];
setSelectedFiles(prev => prev.filter(id => id !== (file.id || file.name)));
}, [handleRemoveFile, recentFiles]);
useEffect(() => {
if (showRecentFiles) {
refreshRecentFiles();
}
}, [showRecentFiles, refreshRecentFiles]);
// Get default title and subtitle from translations if not provided
const displayTitle = title || t("fileUpload.selectFiles", "Select files");
@@ -80,7 +132,7 @@ const FileUploadSelector = ({
return (
<>
<Stack align="center" gap="xl">
<Stack align="center" gap="sm">
{/* Title and description */}
<Stack align="center" gap="md">
<UploadFileIcon style={{ fontSize: 64 }} />
@@ -94,27 +146,14 @@ const FileUploadSelector = ({
{/* Action buttons */}
<Stack align="center" gap="md" w="100%">
<Button
variant="filled"
size="lg"
onClick={() => setShowFilePickerModal(true)}
disabled={disabled || sharedFiles.length === 0}
loading={loading}
>
{loading ? "Loading..." : `Load from Storage (${sharedFiles.length} files available)`}
</Button>
<Text size="md" c="dimmed">
{t("fileUpload.or", "or")}
</Text>
{showDropzone ? (
<Dropzone
onDrop={handleFileUpload}
onDrop={handleNewFileUpload}
accept={accept}
multiple={true}
disabled={disabled || loading}
style={{ width: '100%', minHeight: 120 }}
style={{ width: '100%', height: "5rem" }}
activateOnClick={true}
>
<Center>
@@ -123,7 +162,9 @@ const FileUploadSelector = ({
{t("fileUpload.dropFilesHere", "Drop files here or click to upload")}
</Text>
<Text size="sm" c="dimmed">
{accept.includes('application/pdf')
{accept.includes('application/pdf') && accept.includes('application/zip')
? t("fileUpload.pdfAndZipFiles", "PDF and ZIP files")
: accept.includes('application/pdf')
? t("fileUpload.pdfFilesOnly", "PDF files only")
: t("fileUpload.supportedFileTypes", "Supported file types")
}
@@ -142,7 +183,7 @@ const FileUploadSelector = ({
>
{t("fileUpload.uploadFiles", "Upload Files")}
</Button>
{/* Manual file input as backup */}
<input
ref={fileInputRef}
@@ -155,15 +196,46 @@ const FileUploadSelector = ({
</Stack>
)}
</Stack>
</Stack>
{/* File Picker Modal */}
<FilePickerModal
opened={showFilePickerModal}
onClose={() => setShowFilePickerModal(false)}
storedFiles={sharedFiles}
onSelectFiles={handleStorageSelection}
/>
{/* Recent Files Section */}
{showRecentFiles && recentFiles.length > 0 && (
<Box w="100%" >
<Divider my="md" />
<Text size="lg" fw={500} mb="md">
{t("fileUpload.recentFiles", "Recent Files")}
</Text>
<MultiSelectControls
selectedCount={selectedFiles.length}
onClearSelection={selectionHandlers.clearSelection}
onAddToUpload={handleSelectedRecentFiles}
onDeleteAll={async () => {
await Promise.all(recentFiles.map(async (file) => {
await fileStorage.deleteFile(file.id || file.name);
}));
setRecentFiles([]);
setSelectedFiles([]);
}}
/>
<FileGrid
files={recentFiles}
onDoubleClick={handleRecentFileSelection}
onSelect={selectionHandlers.toggleSelection}
onRemove={handleRemoveFileByIndex}
selectedFiles={selectedFiles}
showSearch={true}
showSort={true}
onDeleteAll={async () => {
await Promise.all(recentFiles.map(async (file) => {
await fileStorage.deleteFile(file.id || file.name);
}));
setRecentFiles([]);
setSelectedFiles([]);
}}
/>
</Box>
)}
</Stack>
</>
);
};

View File

@@ -0,0 +1,88 @@
import React from "react";
import { Box, Group, Text, Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
interface MultiSelectControlsProps {
selectedCount: number;
onClearSelection: () => void;
onOpenInFileEditor?: () => void;
onOpenInPageEditor?: () => void;
onAddToUpload?: () => void;
onDeleteAll?: () => void;
}
const MultiSelectControls = ({
selectedCount,
onClearSelection,
onOpenInFileEditor,
onOpenInPageEditor,
onAddToUpload,
onDeleteAll
}: MultiSelectControlsProps) => {
const { t } = useTranslation();
if (selectedCount === 0) return null;
return (
<Box mb="md" p="md" style={{ backgroundColor: 'var(--mantine-color-blue-0)', borderRadius: 8 }}>
<Group justify="space-between">
<Text size="sm">
{selectedCount} {t("fileManager.filesSelected", "files selected")}
</Text>
<Group>
<Button
size="xs"
variant="light"
onClick={onClearSelection}
>
{t("fileManager.clearSelection", "Clear Selection")}
</Button>
{onAddToUpload && (
<Button
size="xs"
color="green"
onClick={onAddToUpload}
>
{t("fileManager.addToUpload", "Add to Upload")}
</Button>
)}
{onOpenInFileEditor && (
<Button
size="xs"
color="orange"
onClick={onOpenInFileEditor}
disabled={selectedCount === 0}
>
{t("fileManager.openInFileEditor", "Open in File Editor")}
</Button>
)}
{onOpenInPageEditor && (
<Button
size="xs"
color="blue"
onClick={onOpenInPageEditor}
disabled={selectedCount === 0}
>
{t("fileManager.openInPageEditor", "Open in Page Editor")}
</Button>
)}
{onDeleteAll && (
<Button
size="xs"
color="red"
onClick={onDeleteAll}
>
{t("fileManager.deleteAll", "Delete All")}
</Button>
)}
</Group>
</Group>
</Box>
);
};
export default MultiSelectControls;

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
import { useFileContext } from '../../contexts/FileContext';
interface NavigationWarningModalProps {
onApplyAndContinue?: () => Promise<void>;
onExportAndContinue?: () => Promise<void>;
}
const NavigationWarningModal = ({
onApplyAndContinue,
onExportAndContinue
}: NavigationWarningModalProps) => {
const {
showNavigationWarning,
hasUnsavedChanges,
confirmNavigation,
cancelNavigation,
setHasUnsavedChanges
} = useFileContext();
const handleKeepWorking = () => {
cancelNavigation();
};
const handleDiscardChanges = () => {
setHasUnsavedChanges(false);
confirmNavigation();
};
const handleApplyAndContinue = async () => {
if (onApplyAndContinue) {
await onApplyAndContinue();
}
setHasUnsavedChanges(false);
confirmNavigation();
};
const handleExportAndContinue = async () => {
if (onExportAndContinue) {
await onExportAndContinue();
}
setHasUnsavedChanges(false);
confirmNavigation();
};
if (!hasUnsavedChanges) {
return null;
}
return (
<Modal
opened={showNavigationWarning}
onClose={handleKeepWorking}
title="Unsaved Changes"
centered
closeOnClickOutside={false}
closeOnEscape={false}
>
<Stack gap="md">
<Text>
You have unsaved changes to your PDF. What would you like to do?
</Text>
<Group justify="flex-end" gap="sm">
<Button
variant="light"
color="gray"
onClick={handleKeepWorking}
>
Keep Working
</Button>
<Button
variant="light"
color="red"
onClick={handleDiscardChanges}
>
Discard Changes
</Button>
{onApplyAndContinue && (
<Button
variant="light"
color="blue"
onClick={handleApplyAndContinue}
>
Apply & Continue
</Button>
)}
{onExportAndContinue && (
<Button
color="green"
onClick={handleExportAndContinue}
>
Export & Continue
</Button>
)}
</Group>
</Stack>
</Modal>
);
};
export default NavigationWarningModal;

View File

@@ -0,0 +1,104 @@
import React from 'react';
import { Box, Group, Stack } from '@mantine/core';
interface SkeletonLoaderProps {
type: 'pageGrid' | 'fileGrid' | 'controls' | 'viewer';
count?: number;
animated?: boolean;
}
const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({
type,
count = 8,
animated = true
}) => {
const animationStyle = animated ? { animation: 'pulse 2s infinite' } : {};
const renderPageGridSkeleton = () => (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
gap: '1rem'
}}>
{Array.from({ length: count }).map((_, i) => (
<Box
key={i}
w="100%"
h={240}
bg="gray.1"
style={{
borderRadius: '8px',
...animationStyle,
animationDelay: animated ? `${i * 0.1}s` : undefined
}}
/>
))}
</div>
);
const renderFileGridSkeleton = () => (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: '1rem'
}}>
{Array.from({ length: count }).map((_, i) => (
<Box
key={i}
w="100%"
h={280}
bg="gray.1"
style={{
borderRadius: '8px',
...animationStyle,
animationDelay: animated ? `${i * 0.1}s` : undefined
}}
/>
))}
</div>
);
const renderControlsSkeleton = () => (
<Group mb="md">
<Box w={150} h={36} bg="gray.1" style={{ borderRadius: 4, ...animationStyle }} />
<Box w={120} h={36} bg="gray.1" style={{ borderRadius: 4, ...animationStyle }} />
<Box w={100} h={36} bg="gray.1" style={{ borderRadius: 4, ...animationStyle }} />
</Group>
);
const renderViewerSkeleton = () => (
<Stack gap="md" h="100%">
{/* Toolbar skeleton */}
<Group>
<Box w={40} h={40} bg="gray.1" style={{ borderRadius: 4, ...animationStyle }} />
<Box w={40} h={40} bg="gray.1" style={{ borderRadius: 4, ...animationStyle }} />
<Box w={80} h={40} bg="gray.1" style={{ borderRadius: 4, ...animationStyle }} />
<Box w={40} h={40} bg="gray.1" style={{ borderRadius: 4, ...animationStyle }} />
</Group>
{/* Main content skeleton */}
<Box
flex={1}
bg="gray.1"
style={{
borderRadius: '8px',
...animationStyle
}}
/>
</Stack>
);
switch (type) {
case 'pageGrid':
return renderPageGridSkeleton();
case 'fileGrid':
return renderFileGridSkeleton();
case 'controls':
return renderControlsSkeleton();
case 'viewer':
return renderViewerSkeleton();
default:
return null;
}
};
export default SkeletonLoader;

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Button, SegmentedControl } from "@mantine/core";
import React, { useState, useCallback } from "react";
import { Button, SegmentedControl, Loader } from "@mantine/core";
import { useRainbowThemeContext } from "./RainbowThemeProvider";
import LanguageSelector from "./LanguageSelector";
import rainbowStyles from '../../styles/rainbow.module.css';
@@ -8,15 +8,19 @@ import LightModeIcon from '@mui/icons-material/LightMode';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import VisibilityIcon from "@mui/icons-material/Visibility";
import EditNoteIcon from "@mui/icons-material/EditNote";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import FolderIcon from "@mui/icons-material/Folder";
import { Group } from "@mantine/core";
const VIEW_OPTIONS = [
// This will be created inside the component to access switchingTo
const createViewOptions = (switchingTo: string | null) => [
{
label: (
<Group gap={5}>
<VisibilityIcon fontSize="small" />
{switchingTo === "viewer" ? (
<Loader size="xs" />
) : (
<VisibilityIcon fontSize="small" />
)}
</Group>
),
value: "viewer",
@@ -24,7 +28,11 @@ const VIEW_OPTIONS = [
{
label: (
<Group gap={4}>
<EditNoteIcon fontSize="small" />
{switchingTo === "pageEditor" ? (
<Loader size="xs" />
) : (
<EditNoteIcon fontSize="small" />
)}
</Group>
),
value: "pageEditor",
@@ -32,15 +40,11 @@ const VIEW_OPTIONS = [
{
label: (
<Group gap={4}>
<InsertDriveFileIcon fontSize="small" />
</Group>
),
value: "fileManager",
},
{
label: (
<Group gap={4}>
<FolderIcon fontSize="small" />
{switchingTo === "fileEditor" ? (
<Loader size="xs" />
) : (
<FolderIcon fontSize="small" />
)}
</Group>
),
value: "fileEditor",
@@ -50,13 +54,34 @@ const VIEW_OPTIONS = [
interface TopControlsProps {
currentView: string;
setCurrentView: (view: string) => void;
selectedToolKey?: string | null;
}
const TopControls = ({
currentView,
setCurrentView,
selectedToolKey,
}: TopControlsProps) => {
const { themeMode, isRainbowMode, isToggleDisabled, toggleTheme } = useRainbowThemeContext();
const [switchingTo, setSwitchingTo] = useState<string | null>(null);
const isToolSelected = selectedToolKey !== null;
const handleViewChange = useCallback((view: string) => {
// Show immediate feedback
setSwitchingTo(view);
// Defer the heavy view change to next frame so spinner can render
requestAnimationFrame(() => {
// Give the spinner one more frame to show
requestAnimationFrame(() => {
setCurrentView(view);
// Clear the loading state after view change completes
setTimeout(() => setSwitchingTo(null), 300);
});
});
}, [setCurrentView]);
const getThemeIcon = () => {
if (isRainbowMode) return <AutoAwesomeIcon className={rainbowStyles.rainbowText} />;
@@ -66,7 +91,9 @@ const TopControls = ({
return (
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
<div className="absolute left-4 top-1/2 -translate-y-1/2 pointer-events-auto flex gap-2 items-center">
<div className={`absolute left-4 pointer-events-auto flex gap-2 items-center ${
isToolSelected ? 'top-4' : 'top-1/2 -translate-y-1/2'
}`}>
<Button
onClick={toggleTheme}
variant="subtle"
@@ -87,18 +114,24 @@ const TopControls = ({
</Button>
<LanguageSelector />
</div>
<div className="flex justify-center items-center h-full pointer-events-auto">
<SegmentedControl
data={VIEW_OPTIONS}
value={currentView}
onChange={setCurrentView}
color="blue"
radius="xl"
size="md"
fullWidth
className={isRainbowMode ? rainbowStyles.rainbowSegmentedControl : ''}
/>
</div>
{!isToolSelected && (
<div className="flex justify-center items-center h-full pointer-events-auto">
<SegmentedControl
data={createViewOptions(switchingTo)}
value={currentView}
onChange={handleViewChange}
color="blue"
radius="xl"
size="md"
fullWidth
className={isRainbowMode ? rainbowStyles.rainbowSegmentedControl : ''}
style={{
transition: 'all 0.2s ease',
opacity: switchingTo ? 0.8 : 1,
}}
/>
</div>
)}
</div>
);
};