From 57a0f537b2c71985b83b1f924ef53c2545cfa26f Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 18 Sep 2025 18:57:35 +0100 Subject: [PATCH] revert createToolFlow and lean on the accordion steps pattern, blur the preview when no stamp is selected, other --- .../public/locales/en-GB/translation.json | 3 +- .../public/locales/en-US/translation.json | 3 +- .../src/components/shared/ObscuredOverlay.tsx | 50 +++++++++++ .../ObscuredOverlay.module.css | 41 +++++++++ .../tools/addStamp/StampPreview.tsx | 16 ++-- .../tools/addStamp/StampPreviewUtils.ts | 35 +++++++- .../tools/addStamp/useAddStampParameters.ts | 2 + .../tools/shared/SingleExpansionContext.tsx | 41 --------- .../tools/shared/createToolFlow.tsx | 67 +++++---------- .../tools/shared/useSingleExpandController.ts | 65 -------------- frontend/src/tools/AddStamp.tsx | 85 +++++++++++++------ 11 files changed, 214 insertions(+), 194 deletions(-) create mode 100644 frontend/src/components/shared/ObscuredOverlay.tsx create mode 100644 frontend/src/components/shared/ObscuredOverlay/ObscuredOverlay.module.css delete mode 100644 frontend/src/components/tools/shared/SingleExpansionContext.tsx delete mode 100644 frontend/src/components/tools/shared/useSingleExpandController.ts diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 9d53d472a..e94fba47d 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -2176,7 +2176,8 @@ "overrideY": "Override Y Coordinate", "customMargin": "Custom Margin", "customColor": "Custom Text Colour", - "submit": "Submit" + "submit": "Submit", + "noStampSelected": "No stamp selected. Return to Step 3." }, "removeImagePdf": { "tags": "Remove Image,Page operations,Back end,server side" diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index a58268664..ba95d6c77 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -1467,7 +1467,8 @@ "overrideY": "Override Y Coordinate", "customMargin": "Custom Margin", "customColor": "Custom Text Color", - "submit": "Submit" + "submit": "Submit", + "noStampSelected": "No stamp selected. Return to Step 3." }, "removeImagePdf": { "tags": "Remove Image,Page operations,Back end,server side" diff --git a/frontend/src/components/shared/ObscuredOverlay.tsx b/frontend/src/components/shared/ObscuredOverlay.tsx new file mode 100644 index 000000000..0e918151f --- /dev/null +++ b/frontend/src/components/shared/ObscuredOverlay.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import styles from './ObscuredOverlay/ObscuredOverlay.module.css'; + +type ObscuredOverlayProps = { + obscured: boolean; + overlayMessage?: React.ReactNode; + buttonText?: string; + onButtonClick?: () => void; + children: React.ReactNode; + // Optional border radius for the overlay container. If undefined, no radius is applied. + borderRadius?: string | number; +}; + +export default function ObscuredOverlay({ + obscured, + overlayMessage, + buttonText, + onButtonClick, + children, + borderRadius, +}: ObscuredOverlayProps) { + return ( +
+ {children} + {obscured && ( +
+
+ {overlayMessage && ( +
+ {overlayMessage} +
+ )} + {buttonText && onButtonClick && ( + + )} +
+
+ )} +
+ ); +} + + diff --git a/frontend/src/components/shared/ObscuredOverlay/ObscuredOverlay.module.css b/frontend/src/components/shared/ObscuredOverlay/ObscuredOverlay.module.css new file mode 100644 index 000000000..5651993c4 --- /dev/null +++ b/frontend/src/components/shared/ObscuredOverlay/ObscuredOverlay.module.css @@ -0,0 +1,41 @@ +.container { + position: relative; +} + +.overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding: 16px; + color: #ffffff; + font-weight: 600; + background: rgba(16, 18, 27, 0.55); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + border: 1px solid rgba(255, 255, 255, 0.06); + z-index: 2; +} + +.overlayContent { + display: flex; + flex-direction: column; + gap: 12px; + align-items: center; +} + +.overlayMessage { + color: #ffffff; + font-weight: 600; +} + +.overlayButton { + padding: 8px 12px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.06); + color: #ffffff; + cursor: pointer; +} diff --git a/frontend/src/components/tools/addStamp/StampPreview.tsx b/frontend/src/components/tools/addStamp/StampPreview.tsx index 62eed20b4..66ad3c2f0 100644 --- a/frontend/src/components/tools/addStamp/StampPreview.tsx +++ b/frontend/src/components/tools/addStamp/StampPreview.tsx @@ -3,7 +3,6 @@ import { AddStampParameters } from './useAddStampParameters'; import { pdfWorkerManager } from '../../../services/pdfWorkerManager'; import { useThumbnailGeneration } from '../../../hooks/useThumbnailGeneration'; import { A4_ASPECT_RATIO, getFirstSelectedPage, getFontFamily, computeStampPreviewStyle } from './StampPreviewUtils'; -import FitText from '../../shared/FitText'; import styles from './StampPreview.module.css'; type Props = { @@ -87,7 +86,7 @@ export default function StampPreview({ parameters, onParameterChange, file, show } try { const pageNumber = Math.max(1, getFirstSelectedPage(parameters.pageNumbers)); - const pageId = `${file.name}:page:${pageNumber}`; + const pageId = `${file.name}:${file.size}:${file.lastModified}:page:${pageNumber}`; const thumb = await requestThumbnail(pageId, file, pageNumber); if (isActive) setPageThumbnail(thumb || null); } catch { @@ -277,18 +276,17 @@ export default function StampPreview({ parameters, onParameterChange, file, show style={style.item as React.CSSProperties} > {(parameters.stampText || '').split('\n').map((line, idx) => ( - + > + {line || '\u00A0'} + ))} {itemHandles} diff --git a/frontend/src/components/tools/addStamp/StampPreviewUtils.ts b/frontend/src/components/tools/addStamp/StampPreviewUtils.ts index 17a4f1bfe..3ee7811ea 100644 --- a/frontend/src/components/tools/addStamp/StampPreviewUtils.ts +++ b/frontend/src/components/tools/addStamp/StampPreviewUtils.ts @@ -168,13 +168,45 @@ export function computeStampPreviewStyle( const xPts = calcX(); const yPts = calcY(); const xPx = xPts * scaleX; - const yPx = yPts * scaleY; + let yPx = yPts * scaleY; + // Vertical correction: text appears lower in preview vs output for middle/bottom rows + if (parameters.stampType === 'text') { + try { + const rootFontSizePx = parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16; + const middleRowOffsetPx = 1 * rootFontSizePx; + const bottomRowOffsetPx = 1.25 * rootFontSizePx; + const rowIndex = Math.floor((position - 1) / 3); + if (rowIndex === 1) { + yPx += middleRowOffsetPx; + } else if (rowIndex === 2) { + yPx += bottomRowOffsetPx; + } + } catch (e) { + console.error(e); + } + } const widthPx = widthPtsContent * scaleX; const heightPx = heightPtsContent * scaleY; const opacity = Math.max(0, Math.min(1, parameters.opacity / 100)); const displayOpacity = opacity; + // Horizontal alignment inside the preview item for text stamps + let alignItems: 'flex-start' | 'center' | 'flex-end' = 'flex-start'; + if (parameters.stampType === 'text') { + const colIndex = position % 3; // 1: left, 2: center, 0: right + switch (colIndex) { + case 2: // center column + alignItems = 'center'; + break; + case 0: // right column + alignItems = 'flex-end'; + break; + default: + alignItems = 'flex-start'; + } + } + return { container: { position: 'relative', @@ -198,6 +230,7 @@ export function computeStampPreviewStyle( flexDirection: 'column', justifyContent: 'flex-start', lineHeight: 1, + alignItems, cursor: showQuickGrid ? 'default' : 'move', pointerEvents: showQuickGrid ? 'none' : 'auto', } diff --git a/frontend/src/components/tools/addStamp/useAddStampParameters.ts b/frontend/src/components/tools/addStamp/useAddStampParameters.ts index c448b7012..f11d33d5a 100644 --- a/frontend/src/components/tools/addStamp/useAddStampParameters.ts +++ b/frontend/src/components/tools/addStamp/useAddStampParameters.ts @@ -15,6 +15,7 @@ export interface AddStampParameters extends BaseParameters { customMargin: 'small' | 'medium' | 'large' | 'x-large'; customColor: string; pageNumbers: string; + _activePill: 'fontSize' | 'rotation' | 'opacity'; } export const defaultParameters: AddStampParameters = { @@ -30,6 +31,7 @@ export const defaultParameters: AddStampParameters = { customMargin: 'medium', customColor: '#d3d3d3', pageNumbers: '1', + _activePill: 'fontSize', }; export type AddStampParametersHook = BaseParametersHook; diff --git a/frontend/src/components/tools/shared/SingleExpansionContext.tsx b/frontend/src/components/tools/shared/SingleExpansionContext.tsx deleted file mode 100644 index 770a6205f..000000000 --- a/frontend/src/components/tools/shared/SingleExpansionContext.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { createContext, useContext, useState, useCallback } from 'react'; - -// Context for managing single step expansion -interface SingleExpansionContextType { - expandedStep: string | null; - setExpandedStep: (stepId: string | null) => void; - enabled: boolean; -} - -const SingleExpansionContext = createContext({ - expandedStep: null, - setExpandedStep: (_: string | null) => {}, - enabled: false, -}); - -export const useSingleExpansion = () => useContext(SingleExpansionContext); - -// Provider component for single expansion mode -export const SingleExpansionProvider: React.FC<{ - children: React.ReactNode; - enabled: boolean; - initialExpandedStep?: string | null; -}> = ({ children, enabled, initialExpandedStep = null }) => { - const [expandedStep, setExpandedStep] = useState(initialExpandedStep); - - const handleSetExpandedStep = useCallback((stepId: string | null) => { - setExpandedStep(stepId); - }, []); - - const contextValue: SingleExpansionContextType = { - expandedStep, - setExpandedStep: handleSetExpandedStep, - enabled, - }; - - return ( - - {children} - - ); -}; diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx index 1c56da099..4724648c8 100644 --- a/frontend/src/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -5,8 +5,6 @@ import OperationButton from './OperationButton'; import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation'; import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle'; import { StirlingFile } from '../../../types/fileContext'; -import { SingleExpansionProvider } from './SingleExpansionContext'; -import { useSingleExpandController } from './useSingleExpandController'; export interface FilesStepConfig { selectedFiles: StirlingFile[]; @@ -59,46 +57,38 @@ export interface ToolFlowConfig { executeButton?: ExecuteButtonConfig; review: ReviewStepConfig; forceStepNumbers?: boolean; - maxOneExpanded?: boolean; - initialExpandedStep?: string | null; } -// Hoist ToolFlowContent outside to make it stable across renders -function ToolFlowContent({ config }: { config: ToolFlowConfig }) { +/** + * Creates a flexible tool flow with configurable steps and state management left to the tool. + * Reduces boilerplate while allowing tools to manage their own collapse/expansion logic. + */ +export function createToolFlow(config: ToolFlowConfig) { const steps = createToolSteps(); - const { onToggle, isCollapsed } = useSingleExpandController({ - filesVisible: config.files.isVisible !== false, - stepVisibilities: config.steps.map(s => s.isVisible), - resultsVisible: config.review.isVisible, - }); return ( - + + {/* */} {config.title && } {/* Files Step */} {config.files.isVisible !== false && steps.createFilesStep({ selectedFiles: config.files.selectedFiles, - isCollapsed: isCollapsed('files', config.files.isCollapsed), + isCollapsed: config.files.isCollapsed, minFiles: config.files.minFiles, - onCollapsedClick: () => onToggle('files', config.files.onCollapsedClick) + onCollapsedClick: config.files.onCollapsedClick })} {/* Middle Steps */} - {config.steps.map((stepConfig, index) => { - const stepId = `step-${index}`; - return ( - - {steps.create(stepConfig.title, { - isVisible: stepConfig.isVisible, - isCollapsed: isCollapsed(stepId, stepConfig.isCollapsed), - onCollapsedClick: () => onToggle(stepId, stepConfig.onCollapsedClick), - tooltip: stepConfig.tooltip - }, stepConfig.content)} - - ); - })} + {config.steps.map((stepConfig) => + steps.create(stepConfig.title, { + isVisible: stepConfig.isVisible, + isCollapsed: stepConfig.isCollapsed, + onCollapsedClick: stepConfig.onCollapsedClick, + tooltip: stepConfig.tooltip + }, stepConfig.content) + )} {/* Execute Button */} {config.executeButton && config.executeButton.isVisible !== false && ( @@ -118,28 +108,9 @@ function ToolFlowContent({ config }: { config: ToolFlowConfig }) { operation: config.review.operation, title: config.review.title, onFileClick: config.review.onFileClick, - onUndo: config.review.onUndo, - isCollapsed: isCollapsed('review', false), - onCollapsedClick: () => onToggle('review', undefined) + onUndo: config.review.onUndo })} ); -} - -export interface ToolFlowProps extends ToolFlowConfig {} - -export function ToolFlow(props: ToolFlowProps) { - return ( - - - - ); -} - -export function createToolFlow(config: ToolFlowConfig) { - return ; -} +} \ No newline at end of file diff --git a/frontend/src/components/tools/shared/useSingleExpandController.ts b/frontend/src/components/tools/shared/useSingleExpandController.ts deleted file mode 100644 index f2814738a..000000000 --- a/frontend/src/components/tools/shared/useSingleExpandController.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useEffect, useMemo, useCallback } from 'react'; -import { useSingleExpansion } from './SingleExpansionContext'; - -export function useSingleExpandController(opts: { - filesVisible: boolean; - stepVisibilities: (boolean | undefined)[]; - resultsVisible?: boolean; -}) { - const { enabled, expandedStep, setExpandedStep } = useSingleExpansion(); - - const visibleIds = useMemo( - () => [ - ...(opts.filesVisible === false ? [] : ['files']), - ...opts.stepVisibilities.map((v, i) => (v === false ? null : `step-${i}`)).filter(Boolean) as string[], - ...(opts.resultsVisible ? ['review'] : []), - ], - [opts.filesVisible, opts.stepVisibilities, opts.resultsVisible] - ); - - // If single-expand is turned off, clear selection - useEffect(() => { - if (!enabled && expandedStep !== null) setExpandedStep(null); - }, [enabled]); - - // If the selected step becomes invisible, clear it - useEffect(() => { - if (!enabled) return; - if (expandedStep && !visibleIds.includes(expandedStep)) { - setExpandedStep(null); - } - }, [enabled, expandedStep, visibleIds]); - - // When results become visible, automatically expand them and collapse all others - useEffect(() => { - if (!enabled) return; - if (opts.resultsVisible && expandedStep !== 'review') { - setExpandedStep('review'); - } - }, [enabled, opts.resultsVisible, expandedStep, setExpandedStep]); - - const onToggle = useCallback((stepId: string, original?: () => void) => { - if (enabled) { - // If Files is the only visible step, don't allow it to be collapsed - if (stepId === 'files' && visibleIds.length === 1) { - return; // Don't collapse the only visible step - } - setExpandedStep(expandedStep === stepId ? null : stepId); - } - original?.(); - }, [enabled, expandedStep, setExpandedStep, visibleIds]); - - const isCollapsed = useCallback((stepId: string, original?: boolean) => { - if (!enabled) return original ?? false; - - // If Files is the only visible step, never collapse it - if (stepId === 'files' && visibleIds.length === 1) { - return false; - } - - if (expandedStep == null) return true; - return expandedStep !== stepId; - }, [enabled, expandedStep, visibleIds]); - - return { visibleIds, onToggle, isCollapsed }; -} diff --git a/frontend/src/tools/AddStamp.tsx b/frontend/src/tools/AddStamp.tsx index 850c25542..fd580bc62 100644 --- a/frontend/src/tools/AddStamp.tsx +++ b/frontend/src/tools/AddStamp.tsx @@ -12,14 +12,13 @@ import LocalIcon from "../components/shared/LocalIcon"; import styles from "../components/tools/addStamp/StampPreview.module.css"; import { Tooltip } from "../components/shared/Tooltip"; import ButtonSelector from "../components/shared/ButtonSelector"; +import { useAccordionSteps } from "../hooks/tools/shared/useAccordionSteps"; +import ObscuredOverlay from "../components/shared/ObscuredOverlay"; const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); const { selectedFiles } = useFileSelection(); - const [collapsedType, setCollapsedType] = useState(false); - const [collapsedFormatting, setCollapsedFormatting] = useState(true); - const [collapsedPageSelection, setCollapsedPageSelection] = useState(false); const [quickPositionModeSelected, setQuickPositionModeSelected] = useState(false); const [customPositionModeSelected, setCustomPositionModeSelected] = useState(true); @@ -48,14 +47,34 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const hasFiles = selectedFiles.length > 0; const hasResults = operation.files.length > 0 || operation.downloadUrl !== null; + enum AddStampStep { + NONE = 'none', + PAGE_SELECTION = 'pageSelection', + STAMP_TYPE = 'stampType', + POSITION_FORMATTING = 'positionFormatting' + } + + const accordion = useAccordionSteps({ + noneValue: AddStampStep.NONE, + initialStep: AddStampStep.PAGE_SELECTION, + stateConditions: { + hasFiles, + hasResults + }, + afterResults: () => { + operation.resetResults(); + onPreviewFile?.(null); + } + }); + const getSteps = () => { const steps: any[] = []; // Step 1: File settings (page selection) steps.push({ title: t("AddStampRequest.pageSelection", "Page Selection"), - isCollapsed: hasResults || collapsedPageSelection, - onCollapsedClick: hasResults ? () => operation.resetResults() : () => setCollapsedPageSelection(!collapsedPageSelection), + isCollapsed: accordion.getCollapsedState(AddStampStep.PAGE_SELECTION), + onCollapsedClick: () => accordion.handleStepToggle(AddStampStep.PAGE_SELECTION), isVisible: hasFiles || hasResults, content: ( @@ -72,8 +91,8 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { // Step 2: Type & Content steps.push({ title: t("AddStampRequest.stampType", "Stamp Type"), - isCollapsed: hasResults ? true : collapsedType, - onCollapsedClick: hasResults ? () => operation.resetResults() : () => setCollapsedType(!collapsedType), + isCollapsed: accordion.getCollapsedState(AddStampStep.STAMP_TYPE), + onCollapsedClick: () => accordion.handleStepToggle(AddStampStep.STAMP_TYPE), isVisible: hasFiles || hasResults, content: ( @@ -161,8 +180,8 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { // Step 3: Formatting & Position steps.push({ title: t("AddStampRequest.positionAndFormatting", "Position & Formatting"), - isCollapsed: hasResults ? true : collapsedFormatting, - onCollapsedClick: hasResults ? () => operation.resetResults() : () => setCollapsedFormatting(!collapsedFormatting), + isCollapsed: accordion.getCollapsedState(AddStampStep.POSITION_FORMATTING), + onCollapsedClick: () => accordion.handleStepToggle(AddStampStep.POSITION_FORMATTING), isVisible: hasFiles || hasResults, content: ( @@ -201,27 +220,27 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
@@ -229,7 +248,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
{/* Single slider bound to selected pill */} - {(params.parameters as any)._activePill === 'fontSize' && ( + {params.parameters._activePill === 'fontSize' && ( {params.parameters.stampType === 'image' @@ -259,7 +278,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { )} - {(params.parameters as any)._activePill === 'rotation' && ( + {params.parameters._activePill === 'rotation' && ( {t('AddStampRequest.rotation', 'Rotation')} @@ -285,7 +304,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { )} - {(params.parameters as any)._activePill === 'opacity' && ( + {params.parameters._activePill === 'opacity' && ( {t('AddStampRequest.opacity', 'Opacity')} @@ -340,13 +359,26 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { )} - {/* Unified preview; when in quick mode, overlay grid inside preview */} - + {/* Unified preview wrapped with obscured overlay if no stamp selected in step 4 */} + + {t('AddStampRequest.noStampSelected', 'No stamp selected. Return to Step 3.')} + + } + > + + ), }); @@ -377,9 +409,6 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { onPreviewFile?.(null); }, }, - forceStepNumbers: true, - maxOneExpanded: true, - initialExpandedStep: "files" }); };