mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-04-22 23:08:53 +02:00
use clean 3 card design for landing page (#6084)
<img width="2056" height="1080" alt="Screenshot 2026-04-08 at 1 26 58 PM" src="https://github.com/user-attachments/assets/e834988b-c3ab-4633-bf15-9fe0457d0029" /> <img width="2056" height="1080" alt="Screenshot 2026-04-08 at 1 27 12 PM" src="https://github.com/user-attachments/assets/adfebd95-ca59-4de0-9336-b1e2dc1dc5fe" />
This commit is contained in:
@@ -4287,6 +4287,8 @@ welcomeTitle = "You've been invited!"
|
||||
|
||||
[landing]
|
||||
addFiles = "Add Files"
|
||||
heroSubtitle = "Drop in or add an existing PDF to get started."
|
||||
heroTitle = "Stirling PDF"
|
||||
mobileUpload = "Upload from Mobile"
|
||||
openFromComputer = "Open from computer"
|
||||
uploadFromComputer = "Upload from computer"
|
||||
|
||||
62
frontend/src/core/components/shared/LandingActions.tsx
Normal file
62
frontend/src/core/components/shared/LandingActions.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { Button, Group, Tooltip, ActionIcon } from '@mantine/core';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology';
|
||||
import { useFileActionIcons } from '@app/hooks/useFileActionIcons';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
|
||||
type LandingActionsProps = {
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
onUploadClick: () => void;
|
||||
onMobileUploadClick: () => void;
|
||||
onFileSelect: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
export function LandingActions({ fileInputRef, onUploadClick, onMobileUploadClick, onFileSelect }: LandingActionsProps) {
|
||||
const terminology = useFileActionTerminology();
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const icons = useFileActionIcons();
|
||||
const { config } = useAppConfig();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group gap="sm" justify="center" wrap="wrap" mb="xs">
|
||||
<Button
|
||||
classNames={{ root: 'landing-btn-primary' }}
|
||||
leftSection={<LocalIcon icon={icons.uploadIconName} width="1rem" height="1rem" style={{ color: 'white' }} />}
|
||||
onClick={(e) => { e.stopPropagation(); onUploadClick(); }}
|
||||
>
|
||||
{terminology.uploadFromComputer}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
classNames={{ root: 'landing-btn-secondary' }}
|
||||
leftSection={<LocalIcon icon="add" width="1rem" height="1rem" className="text-[var(--accent-interactive)]" />}
|
||||
onClick={(e) => { e.stopPropagation(); openFilesModal(); }}
|
||||
>
|
||||
{terminology.addFiles}
|
||||
</Button>
|
||||
|
||||
{config?.enableMobileScanner && !isMobile && (
|
||||
<Tooltip label={terminology.mobileUpload} position="bottom">
|
||||
<ActionIcon
|
||||
size="lg"
|
||||
variant="default"
|
||||
radius="md"
|
||||
aria-label={terminology.mobileUpload}
|
||||
classNames={{ root: 'landing-btn-secondary landing-btn-icon' }}
|
||||
onClick={(e) => { e.stopPropagation(); onMobileUploadClick(); }}
|
||||
>
|
||||
<LocalIcon icon="qr-code-rounded" width="1.25rem" height="1.25rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
<input ref={fileInputRef} type="file" multiple onChange={onFileSelect} style={{ display: 'none' }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
47
frontend/src/core/components/shared/LandingDocumentStack.tsx
Normal file
47
frontend/src/core/components/shared/LandingDocumentStack.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/** Decorative stack only: window dots + grey bars — no text or i18n (avoids keys showing in the UI). */
|
||||
export function LandingDocumentStack() {
|
||||
const bar = (widthPct: number, heightPx: number, marginBottom: number) => ({
|
||||
width: `${widthPct}%`,
|
||||
height: heightPx,
|
||||
marginBottom: marginBottom || undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<div aria-hidden className="landing-stack">
|
||||
<div className="landing-sheet landing-sheet--back landing-sheet--left">
|
||||
<div className="landing-sheet-side-body">
|
||||
<div className="landing-bar landing-bar--strong" style={bar(100, 10, 12)} />
|
||||
<div className="landing-bar" style={bar(80, 8, 8)} />
|
||||
<div className="landing-bar" style={bar(100, 8, 8)} />
|
||||
<div className="landing-bar" style={bar(60, 8, 0)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="landing-sheet landing-sheet--front">
|
||||
<div className="landing-sheet-header">
|
||||
<div className="landing-sheet-dot" style={{ backgroundColor: 'rgba(255,255,255,0.35)' }} />
|
||||
<div className="landing-sheet-dot" style={{ backgroundColor: 'rgba(255,255,255,0.25)' }} />
|
||||
<div className="landing-sheet-dot" style={{ backgroundColor: 'rgba(255,255,255,0.15)' }} />
|
||||
</div>
|
||||
<div className="landing-sheet-body">
|
||||
<div className="landing-bar landing-bar--strong" style={bar(100, 10, 10)} />
|
||||
<div className="landing-bar" style={bar(80, 7, 6)} />
|
||||
<div className="landing-bar" style={bar(100, 7, 6)} />
|
||||
<div className="landing-bar" style={bar(66, 7, 6)} />
|
||||
<div className="landing-bar" style={bar(100, 7, 6)} />
|
||||
<div className="landing-bar" style={bar(88, 7, 6)} />
|
||||
<div className="landing-bar" style={bar(72, 7, 0)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="landing-sheet landing-sheet--back landing-sheet--right">
|
||||
<div className="landing-sheet-side-body">
|
||||
<div className="landing-bar landing-bar--strong" style={bar(100, 10, 12)} />
|
||||
<div className="landing-bar" style={bar(75, 8, 8)} />
|
||||
<div className="landing-bar" style={bar(100, 8, 8)} />
|
||||
<div className="landing-bar" style={bar(80, 8, 0)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
137
frontend/src/core/components/shared/LandingPage.css
Normal file
137
frontend/src/core/components/shared/LandingPage.css
Normal file
@@ -0,0 +1,137 @@
|
||||
/* ============================================================
|
||||
Landing Page styles.
|
||||
All custom properties are defined in theme.css.
|
||||
============================================================ */
|
||||
|
||||
/* ── Hero text ───────────────────────────────────────────── */
|
||||
.landing-title {
|
||||
margin: 0;
|
||||
margin-top: 1.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
font-size: 2.125rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.landing-subtitle {
|
||||
margin: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.5;
|
||||
max-width: 28rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ── Document stack ──────────────────────────────────────── */
|
||||
.landing-stack {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: var(--landing-stack-w);
|
||||
min-width: var(--landing-stack-w);
|
||||
height: var(--landing-stack-h);
|
||||
min-height: var(--landing-stack-h);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Sheets — static white, never change with theme */
|
||||
.landing-sheet {
|
||||
position: absolute;
|
||||
border-radius: 12px;
|
||||
background-color: #ffffff;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.landing-sheet--back {
|
||||
width: 128px;
|
||||
height: 160px;
|
||||
transform-origin: bottom center;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: var(--landing-doc-shadow-back-idle);
|
||||
}
|
||||
|
||||
.landing-sheet--left {
|
||||
left: 8px;
|
||||
top: 12px;
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
|
||||
.landing-sheet--right {
|
||||
right: 8px;
|
||||
top: 12px;
|
||||
transform: rotate(8deg);
|
||||
}
|
||||
|
||||
.landing-sheet--front {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
width: 144px;
|
||||
height: 176px;
|
||||
margin-left: -72px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--landing-doc-shadow-front-idle);
|
||||
}
|
||||
|
||||
.landing-sheet-header {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
border-radius: 12px 12px 0 0;
|
||||
background: var(--landing-hero-gradient);
|
||||
}
|
||||
|
||||
.landing-sheet-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.landing-sheet-body {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.landing-sheet-side-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Bars — static light colours, never change with theme */
|
||||
.landing-bar {
|
||||
border-radius: 9999px;
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
.landing-bar--strong {
|
||||
background-color: #d1d5db;
|
||||
}
|
||||
|
||||
/* ── Action buttons ──────────────────────────────────────── */
|
||||
.landing-btn-primary {
|
||||
background: var(--landing-hero-gradient) !important;
|
||||
color: #ffffff !important;
|
||||
border: none !important;
|
||||
border-radius: 0.75rem !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.landing-btn-secondary {
|
||||
border-radius: 0.75rem !important;
|
||||
font-weight: 600 !important;
|
||||
border-color: var(--landing-button-border, var(--border-default)) !important;
|
||||
background-color: var(--landing-button-bg, var(--bg-surface)) !important;
|
||||
color: var(--landing-button-color, var(--text-primary)) !important;
|
||||
}
|
||||
.landing-btn-secondary:hover {
|
||||
background-color: var(--landing-button-hover-bg, var(--landing-button-bg, var(--bg-surface))) !important;
|
||||
}
|
||||
|
||||
/* Icon-only variant: accent colour instead of button text colour */
|
||||
.landing-btn-icon {
|
||||
color: var(--accent-interactive) !important;
|
||||
}
|
||||
@@ -1,51 +1,30 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Container, Button, Group, useMantineColorScheme, ActionIcon, Tooltip } from '@mantine/core';
|
||||
import React, { useState } from 'react';
|
||||
import { Container } from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFileHandler } from '@app/hooks/useFileHandler';
|
||||
import { useFilesModalContext } from '@app/contexts/FilesModalContext';
|
||||
import { useLogoPath } from '@app/hooks/useLogoPath';
|
||||
import { useLogoAssets } from '@app/hooks/useLogoAssets';
|
||||
import { useLogoVariant } from '@app/hooks/useLogoVariant';
|
||||
import { useFileManager } from '@app/hooks/useFileManager';
|
||||
import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology';
|
||||
import { useFileActionIcons } from '@app/hooks/useFileActionIcons';
|
||||
import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||
import MobileUploadModal from '@app/components/shared/MobileUploadModal';
|
||||
import { openFilesFromDisk } from '@app/services/openFilesFromDisk';
|
||||
import { LandingDocumentStack } from '@app/components/shared/LandingDocumentStack';
|
||||
import { LandingActions } from '@app/components/shared/LandingActions';
|
||||
import '@app/components/shared/LandingPage.css';
|
||||
|
||||
const LandingPage = () => {
|
||||
const { addFiles } = useFileHandler();
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const { t } = useTranslation();
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const [isUploadHover, setIsUploadHover] = React.useState(false);
|
||||
const logoPath = useLogoPath();
|
||||
const logoVariant = useLogoVariant();
|
||||
const { wordmark } = useLogoAssets();
|
||||
const { loadRecentFiles } = useFileManager();
|
||||
const [hasRecents, setHasRecents] = React.useState<boolean>(false);
|
||||
const [mobileUploadModalOpen, setMobileUploadModalOpen] = React.useState(false);
|
||||
const { addFiles } = useFileHandler();
|
||||
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const terminology = useFileActionTerminology();
|
||||
const icons = useFileActionIcons();
|
||||
const { config } = useAppConfig();
|
||||
const isMobile = useIsMobile();
|
||||
const [mobileUploadModalOpen, setMobileUploadModalOpen] = useState(false);
|
||||
|
||||
const handleFileDrop = async (files: File[]) => {
|
||||
await addFiles(files);
|
||||
};
|
||||
|
||||
const handleOpenFilesModal = () => {
|
||||
openFilesModal();
|
||||
};
|
||||
|
||||
const handleNativeUploadClick = async () => {
|
||||
const files = await openFilesFromDisk({
|
||||
multiple: true,
|
||||
onFallbackOpen: () => fileInputRef.current?.click()
|
||||
onFallbackOpen: () => fileInputRef.current?.click(),
|
||||
});
|
||||
if (files.length > 0) {
|
||||
await addFiles(files);
|
||||
@@ -57,263 +36,48 @@ const LandingPage = () => {
|
||||
if (files.length > 0) {
|
||||
await addFiles(files);
|
||||
}
|
||||
// Reset the input so the same file can be selected again
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
const handleMobileUploadClick = () => {
|
||||
setMobileUploadModalOpen(true);
|
||||
};
|
||||
|
||||
const handleFilesReceivedFromMobile = async (files: File[]) => {
|
||||
if (files.length > 0) {
|
||||
await addFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
// Determine if the user has any recent files (same source as File Manager)
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const files = await loadRecentFiles();
|
||||
if (isMounted) {
|
||||
setHasRecents((files?.length || 0) > 0);
|
||||
}
|
||||
} catch (_err) {
|
||||
if (isMounted) setHasRecents(false);
|
||||
}
|
||||
})();
|
||||
return () => { isMounted = false; };
|
||||
}, [loadRecentFiles]);
|
||||
|
||||
return (
|
||||
<Container size="70rem" p={0} h="100%" className="flex items-center justify-center" style={{ position: 'relative' }}>
|
||||
{/* White PDF Page Background */}
|
||||
<Container size="70rem" p={0} h="100%" className="flex min-h-0 flex-col" style={{ position: 'relative' }}>
|
||||
<Dropzone
|
||||
onDrop={handleFileDrop}
|
||||
multiple={true}
|
||||
className="w-4/5 flex items-center justify-center h-[95%]"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
bottom: 0,
|
||||
borderRadius: '0.25rem 0.25rem 0 0',
|
||||
filter: 'var(--drop-shadow-filter)',
|
||||
backgroundColor: 'var(--landing-paper-bg)',
|
||||
transition: 'background-color 0.4s ease',
|
||||
}}
|
||||
multiple
|
||||
activateOnClick={false}
|
||||
enablePointerEvents
|
||||
aria-label={terminology.dropFilesHere}
|
||||
className="flex min-h-0 flex-1 cursor-default flex-col items-center justify-center border-none bg-transparent px-4 py-8 shadow-none outline-none"
|
||||
styles={{
|
||||
root: {
|
||||
'&[dataAccept]': {
|
||||
backgroundColor: 'var(--landing-drop-paper-bg)',
|
||||
},
|
||||
border: 'none !important',
|
||||
backgroundColor: 'transparent',
|
||||
overflow: 'visible',
|
||||
'&[data-accept]': { outline: '2px dashed var(--accent-interactive)', outlineOffset: 4 },
|
||||
'&[data-reject]': { outline: '2px dashed var(--mantine-color-red-6)', outlineOffset: 4 },
|
||||
},
|
||||
inner: { overflow: 'visible', display: 'flex', flexDirection: 'column', alignItems: 'center', width: '100%' },
|
||||
}}
|
||||
>
|
||||
{logoVariant === 'modern' && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={logoPath}
|
||||
alt="Stirling PDF Logo"
|
||||
style={{
|
||||
height: 'auto',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`min-h-[45vh] flex flex-col items-center justify-center px-8 py-8 w-full min-w-[30rem] max-w-[calc(100%-2rem)] border transition-all duration-200 dropzone-inner relative`}
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
backgroundColor: 'var(--landing-inner-paper-bg)',
|
||||
borderColor: 'var(--landing-inner-paper-border)',
|
||||
borderWidth: '1px',
|
||||
borderStyle: 'solid',
|
||||
}}
|
||||
>
|
||||
{/* Logo positioned absolutely in top right corner */}
|
||||
<LandingDocumentStack />
|
||||
|
||||
<h1 className="landing-title">{t('landing.heroTitle', 'Stirling PDF')}</h1>
|
||||
<p className="landing-subtitle">{t('landing.heroSubtitle', 'Drop in or add an existing PDF to get started.')}</p>
|
||||
|
||||
{/* Centered content container */}
|
||||
<div className="flex flex-col items-center gap-4 flex-none w-full">
|
||||
{/* Stirling PDF Branding */}
|
||||
<Group gap="xs" align="center">
|
||||
<img
|
||||
src={colorScheme === 'dark' ? wordmark.white : wordmark.grey}
|
||||
alt="Stirling PDF"
|
||||
style={{ height: '2.2rem', width: 'auto' }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Add Files + Native Upload Buttons */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '0.6rem',
|
||||
width: '80%',
|
||||
marginTop: '0.8rem',
|
||||
marginBottom: '0.8rem'
|
||||
}}
|
||||
onMouseLeave={() => setIsUploadHover(false)}
|
||||
>
|
||||
{/* Show both buttons only when recents exist; otherwise show a single Upload button */}
|
||||
{hasRecents && (
|
||||
<>
|
||||
<Button
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '2rem',
|
||||
height: '38px',
|
||||
paddingLeft: isUploadHover ? 0 : '1rem',
|
||||
paddingRight: isUploadHover ? 0 : '1rem',
|
||||
width: isUploadHover ? '58px' : 'calc(100% - 58px - 0.6rem)',
|
||||
minWidth: isUploadHover ? '58px' : undefined,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease'
|
||||
}}
|
||||
onClick={handleOpenFilesModal}
|
||||
onMouseEnter={() => setIsUploadHover(false)}
|
||||
>
|
||||
<LocalIcon icon="add" width="1.5rem" height="1.5rem" className="text-[var(--accent-interactive)]" />
|
||||
{!isUploadHover && (
|
||||
<span>
|
||||
{t('landing.addFiles', 'Add Files')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '1rem',
|
||||
height: '38px',
|
||||
width: isUploadHover ? 'calc(100% - 50px)' : '58px',
|
||||
minWidth: '58px',
|
||||
paddingLeft: isUploadHover ? '1rem' : 0,
|
||||
paddingRight: isUploadHover ? '1rem' : 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease'
|
||||
}}
|
||||
onClick={handleNativeUploadClick}
|
||||
onMouseEnter={() => setIsUploadHover(true)}
|
||||
>
|
||||
<LocalIcon icon={icons.uploadIconName} width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
{isUploadHover && (
|
||||
<span style={{ marginLeft: '.5rem' }}>
|
||||
{terminology.uploadFromComputer}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{config?.enableMobileScanner && !isMobile && (
|
||||
<Tooltip label={t('landing.mobileUpload', 'Upload from Mobile')} position="bottom">
|
||||
<ActionIcon
|
||||
size={38}
|
||||
variant="subtle"
|
||||
onClick={handleMobileUploadClick}
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--accent-interactive)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '1rem',
|
||||
paddingLeft: '0.5rem',
|
||||
paddingRight: '0.5rem',
|
||||
}}
|
||||
>
|
||||
<LocalIcon icon="qr-code-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!hasRecents && (
|
||||
<>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '1rem',
|
||||
height: '38px',
|
||||
width: 'calc(100% - 38px - 0.6rem)',
|
||||
minWidth: '58px',
|
||||
paddingLeft: '1rem',
|
||||
paddingRight: '1rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
onClick={handleNativeUploadClick}
|
||||
>
|
||||
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
<span style={{ marginLeft: '.5rem' }}>
|
||||
{t('landing.uploadFromComputer', 'Upload from computer')}
|
||||
</span>
|
||||
</Button>
|
||||
{config?.enableMobileScanner && !isMobile && (
|
||||
<Tooltip label={t('landing.mobileUpload', 'Upload from Mobile')} position="bottom">
|
||||
<ActionIcon
|
||||
size={38}
|
||||
variant="subtle"
|
||||
onClick={handleMobileUploadClick}
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--accent-interactive)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '1rem',
|
||||
paddingLeft: '0.5rem',
|
||||
paddingRight: '0.5rem',
|
||||
}}
|
||||
>
|
||||
<LocalIcon icon="qr-code-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hidden file input for native file picker */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Instruction Text */}
|
||||
<span
|
||||
className="text-[var(--accent-interactive)]"
|
||||
style={{ fontSize: '.8rem' }}
|
||||
>
|
||||
{terminology.dropFilesHere}
|
||||
</span>
|
||||
</div>
|
||||
<LandingActions
|
||||
fileInputRef={fileInputRef}
|
||||
onUploadClick={() => void handleNativeUploadClick()}
|
||||
onMobileUploadClick={() => setMobileUploadModalOpen(true)}
|
||||
onFileSelect={handleFileSelect}
|
||||
/>
|
||||
</Dropzone>
|
||||
|
||||
<MobileUploadModal
|
||||
opened={mobileUploadModalOpen}
|
||||
onClose={() => setMobileUploadModalOpen(false)}
|
||||
|
||||
@@ -12,6 +12,8 @@ export function useFileActionTerminology() {
|
||||
uploadFile: t('fileUpload.uploadFile', 'Upload File'),
|
||||
upload: t('fileUpload.upload', 'Upload'),
|
||||
dropFilesHere: t('fileUpload.dropFilesHere', 'Drop files here or click the upload button'),
|
||||
addFiles: t('landing.addFiles', 'Add Files'),
|
||||
mobileUpload: t('landing.mobileUpload', 'Upload from Mobile'),
|
||||
uploadFromComputer: t('landing.uploadFromComputer', 'Upload from computer'),
|
||||
download: t('download', 'Download'),
|
||||
downloadAll: t('rightRail.downloadAll', 'Download All'),
|
||||
|
||||
@@ -256,6 +256,25 @@
|
||||
--landing-drop-inner-paper-bg: #BBDEFB;
|
||||
--landing-drop-inner-paper-border: #90CAF9;
|
||||
|
||||
/* landing hero & stack */
|
||||
--landing-hero-gradient: linear-gradient(135deg, #4c8bf5 0%, #3a7be8 100%);
|
||||
--landing-stack-w: 224px;
|
||||
--landing-stack-h: 176px;
|
||||
--landing-stack-glow-bg: radial-gradient(circle, rgba(74,144,226,.18) 0%, transparent 70%);
|
||||
|
||||
/* landing doc stack shadows */
|
||||
--landing-doc-shadow-back-idle: 0 4px 20px rgba(0,0,0,.08), 0 1px 3px rgba(0,0,0,.04);
|
||||
--landing-doc-shadow-back-hover: 0 12px 40px rgba(0,0,0,.15), 0 4px 12px rgba(0,0,0,.08);
|
||||
--landing-doc-shadow-front-idle: 0 8px 30px rgba(0,0,0,.12), 0 4px 12px rgba(0,0,0,.06), 0 0 0 1px rgba(0,0,0,.02);
|
||||
--landing-doc-shadow-front-hover: 0 18px 48px rgba(0,0,0,.18), 0 8px 20px rgba(0,0,0,.1), 0 0 0 1px rgba(0,0,0,.04);
|
||||
|
||||
/* landing action button shadows */
|
||||
--landing-action-transition: transform 0.28s cubic-bezier(0.4,0,0.2,1), box-shadow 0.32s cubic-bezier(0.4,0,0.2,1);
|
||||
--landing-action-shadow-idle: 0 6px 18px rgba(0,0,0,0), 0 2px 6px rgba(0,0,0,0);
|
||||
--landing-action-shadow-hover: 0 6px 18px rgba(0,0,0,.14), 0 2px 6px rgba(0,0,0,.08);
|
||||
--landing-action-primary-shadow-idle: 0 10px 26px rgba(58,123,232,0), 0 4px 12px rgba(0,0,0,0);
|
||||
--landing-action-primary-shadow-hover: 0 10px 26px rgba(58,123,232,.42), 0 4px 12px rgba(0,0,0,.1);
|
||||
|
||||
/* selected file header colors */
|
||||
--header-selected-bg: #1E88E5; /* light mode selected header matches dark */
|
||||
--header-selected-fg: #FFFFFF;
|
||||
@@ -509,15 +528,15 @@
|
||||
--landing-paper-bg: #171A1F;
|
||||
--landing-inner-paper-bg: var(--bg-raised);
|
||||
--landing-inner-paper-border: #2D3237;
|
||||
--landing-button-bg: #2B3037;
|
||||
--landing-button-color: #ffffff;
|
||||
--landing-button-border: #2D3237;
|
||||
--landing-button-hover-bg: #4c525b;
|
||||
|
||||
/* drop state */
|
||||
--landing-drop-paper-bg: #1A2332;
|
||||
--landing-drop-inner-paper-bg: #2A3441;
|
||||
--landing-drop-inner-paper-border: #3A4451;
|
||||
/* landing dark overrides */
|
||||
--landing-stack-glow-bg: radial-gradient(circle, rgba(30,136,229,.22) 0%, transparent 70%);
|
||||
--landing-doc-shadow-back-idle: 0 4px 20px rgba(0,0,0,.35), 0 1px 3px rgba(0,0,0,.25);
|
||||
--landing-doc-shadow-back-hover: 0 14px 44px rgba(0,0,0,.55), 0 6px 16px rgba(0,0,0,.35);
|
||||
--landing-doc-shadow-front-idle: 0 8px 30px rgba(0,0,0,.45), 0 4px 12px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.04);
|
||||
--landing-doc-shadow-front-hover: 0 20px 52px rgba(0,0,0,.6), 0 10px 24px rgba(0,0,0,.4), 0 0 0 1px rgba(255,255,255,.06);
|
||||
--landing-button-color: #ffffff;
|
||||
--landing-button-hover-bg: var(--bg-raised);
|
||||
|
||||
/* selected file header colors for dark */
|
||||
--header-selected-bg: #1E88E5;
|
||||
|
||||
@@ -12,6 +12,8 @@ export function useFileActionTerminology() {
|
||||
uploadFile: t('fileUpload.openFile', 'Open File'),
|
||||
upload: t('fileUpload.open', 'Open'),
|
||||
dropFilesHere: t('fileUpload.dropFilesHereOpen', 'Drop files here or click the open button'),
|
||||
addFiles: t('fileUpload.openFiles', 'Open Files'),
|
||||
mobileUpload: t('landing.mobileUpload', 'Upload from Mobile'),
|
||||
uploadFromComputer: t('landing.openFromComputer', 'Open from computer'),
|
||||
download: t('save', 'Save'),
|
||||
downloadAll: t('rightRail.saveAll', 'Save All'),
|
||||
|
||||
Reference in New Issue
Block a user