diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index a91985ee41..693144ffd6 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -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" diff --git a/frontend/src/core/components/shared/LandingActions.tsx b/frontend/src/core/components/shared/LandingActions.tsx new file mode 100644 index 0000000000..249bd59d0f --- /dev/null +++ b/frontend/src/core/components/shared/LandingActions.tsx @@ -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; + onUploadClick: () => void; + onMobileUploadClick: () => void; + onFileSelect: (event: React.ChangeEvent) => 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 ( + <> + + + + + + {config?.enableMobileScanner && !isMobile && ( + + { e.stopPropagation(); onMobileUploadClick(); }} + > + + + + )} + + + + ); +} diff --git a/frontend/src/core/components/shared/LandingDocumentStack.tsx b/frontend/src/core/components/shared/LandingDocumentStack.tsx new file mode 100644 index 0000000000..3fffd48602 --- /dev/null +++ b/frontend/src/core/components/shared/LandingDocumentStack.tsx @@ -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 ( +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/core/components/shared/LandingPage.css b/frontend/src/core/components/shared/LandingPage.css new file mode 100644 index 0000000000..60c2f3730e --- /dev/null +++ b/frontend/src/core/components/shared/LandingPage.css @@ -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; +} diff --git a/frontend/src/core/components/shared/LandingPage.tsx b/frontend/src/core/components/shared/LandingPage.tsx index a2ad72e2a4..1d9b878f46 100644 --- a/frontend/src/core/components/shared/LandingPage.tsx +++ b/frontend/src/core/components/shared/LandingPage.tsx @@ -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(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(false); - const [mobileUploadModalOpen, setMobileUploadModalOpen] = React.useState(false); + const { addFiles } = useFileHandler(); + const fileInputRef = React.useRef(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 ( - - {/* White PDF Page Background */} + - {logoVariant === 'modern' && ( -
- Stirling PDF Logo -
- )} -
- {/* Logo positioned absolutely in top right corner */} + +

{t('landing.heroTitle', 'Stirling PDF')}

+

{t('landing.heroSubtitle', 'Drop in or add an existing PDF to get started.')}

- {/* Centered content container */} -
- {/* Stirling PDF Branding */} - - Stirling PDF - - - {/* Add Files + Native Upload Buttons */} -
setIsUploadHover(false)} - > - {/* Show both buttons only when recents exist; otherwise show a single Upload button */} - {hasRecents && ( - <> - - - {config?.enableMobileScanner && !isMobile && ( - - - - - - )} - - )} - {!hasRecents && ( - <> - - {config?.enableMobileScanner && !isMobile && ( - - - - - - )} - - )} -
- - {/* Hidden file input for native file picker */} - - -
- - {/* Instruction Text */} - - {terminology.dropFilesHere} - -
+ void handleNativeUploadClick()} + onMobileUploadClick={() => setMobileUploadModalOpen(true)} + onFileSelect={handleFileSelect} + />
+ setMobileUploadModalOpen(false)} diff --git a/frontend/src/core/hooks/useFileActionTerminology.ts b/frontend/src/core/hooks/useFileActionTerminology.ts index 12d992bbe2..6d5434343e 100644 --- a/frontend/src/core/hooks/useFileActionTerminology.ts +++ b/frontend/src/core/hooks/useFileActionTerminology.ts @@ -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'), diff --git a/frontend/src/core/styles/theme.css b/frontend/src/core/styles/theme.css index ec63c0a572..7e30411d6a 100644 --- a/frontend/src/core/styles/theme.css +++ b/frontend/src/core/styles/theme.css @@ -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; diff --git a/frontend/src/desktop/hooks/useFileActionTerminology.ts b/frontend/src/desktop/hooks/useFileActionTerminology.ts index 2176cedae9..fd7399533b 100644 --- a/frontend/src/desktop/hooks/useFileActionTerminology.ts +++ b/frontend/src/desktop/hooks/useFileActionTerminology.ts @@ -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'),