mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-19 18:09:28 +00:00
revert createToolFlow and lean on the accordion steps pattern, blur the preview when no stamp is selected, other
This commit is contained in:
parent
0578782b12
commit
57a0f537b2
@ -2176,7 +2176,8 @@
|
|||||||
"overrideY": "Override Y Coordinate",
|
"overrideY": "Override Y Coordinate",
|
||||||
"customMargin": "Custom Margin",
|
"customMargin": "Custom Margin",
|
||||||
"customColor": "Custom Text Colour",
|
"customColor": "Custom Text Colour",
|
||||||
"submit": "Submit"
|
"submit": "Submit",
|
||||||
|
"noStampSelected": "No stamp selected. Return to Step 3."
|
||||||
},
|
},
|
||||||
"removeImagePdf": {
|
"removeImagePdf": {
|
||||||
"tags": "Remove Image,Page operations,Back end,server side"
|
"tags": "Remove Image,Page operations,Back end,server side"
|
||||||
|
@ -1467,7 +1467,8 @@
|
|||||||
"overrideY": "Override Y Coordinate",
|
"overrideY": "Override Y Coordinate",
|
||||||
"customMargin": "Custom Margin",
|
"customMargin": "Custom Margin",
|
||||||
"customColor": "Custom Text Color",
|
"customColor": "Custom Text Color",
|
||||||
"submit": "Submit"
|
"submit": "Submit",
|
||||||
|
"noStampSelected": "No stamp selected. Return to Step 3."
|
||||||
},
|
},
|
||||||
"removeImagePdf": {
|
"removeImagePdf": {
|
||||||
"tags": "Remove Image,Page operations,Back end,server side"
|
"tags": "Remove Image,Page operations,Back end,server side"
|
||||||
|
50
frontend/src/components/shared/ObscuredOverlay.tsx
Normal file
50
frontend/src/components/shared/ObscuredOverlay.tsx
Normal file
@ -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 (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{children}
|
||||||
|
{obscured && (
|
||||||
|
<div
|
||||||
|
className={styles.overlay}
|
||||||
|
style={{
|
||||||
|
...(borderRadius !== undefined ? { borderRadius } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.overlayContent}>
|
||||||
|
{overlayMessage && (
|
||||||
|
<div className={styles.overlayMessage}>
|
||||||
|
{overlayMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{buttonText && onButtonClick && (
|
||||||
|
<button type="button" onClick={onButtonClick} className={styles.overlayButton}>
|
||||||
|
{buttonText}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
@ -3,7 +3,6 @@ import { AddStampParameters } from './useAddStampParameters';
|
|||||||
import { pdfWorkerManager } from '../../../services/pdfWorkerManager';
|
import { pdfWorkerManager } from '../../../services/pdfWorkerManager';
|
||||||
import { useThumbnailGeneration } from '../../../hooks/useThumbnailGeneration';
|
import { useThumbnailGeneration } from '../../../hooks/useThumbnailGeneration';
|
||||||
import { A4_ASPECT_RATIO, getFirstSelectedPage, getFontFamily, computeStampPreviewStyle } from './StampPreviewUtils';
|
import { A4_ASPECT_RATIO, getFirstSelectedPage, getFontFamily, computeStampPreviewStyle } from './StampPreviewUtils';
|
||||||
import FitText from '../../shared/FitText';
|
|
||||||
import styles from './StampPreview.module.css';
|
import styles from './StampPreview.module.css';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -87,7 +86,7 @@ export default function StampPreview({ parameters, onParameterChange, file, show
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const pageNumber = Math.max(1, getFirstSelectedPage(parameters.pageNumbers));
|
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);
|
const thumb = await requestThumbnail(pageId, file, pageNumber);
|
||||||
if (isActive) setPageThumbnail(thumb || null);
|
if (isActive) setPageThumbnail(thumb || null);
|
||||||
} catch {
|
} catch {
|
||||||
@ -277,18 +276,17 @@ export default function StampPreview({ parameters, onParameterChange, file, show
|
|||||||
style={style.item as React.CSSProperties}
|
style={style.item as React.CSSProperties}
|
||||||
>
|
>
|
||||||
{(parameters.stampText || '').split('\n').map((line, idx) => (
|
{(parameters.stampText || '').split('\n').map((line, idx) => (
|
||||||
<FitText
|
<span
|
||||||
key={idx}
|
key={idx}
|
||||||
text={line || '\u00A0'}
|
|
||||||
lines={1}
|
|
||||||
minimumFontScale={0.5}
|
|
||||||
fontSize={parameters.fontSize}
|
|
||||||
className={styles.textLine}
|
className={styles.textLine}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: getFontFamily(parameters.alphabet),
|
fontFamily: getFontFamily(parameters.alphabet),
|
||||||
|
fontSize: `${Math.max(1, parameters.fontSize / 2)}px`,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
as="span"
|
>
|
||||||
/>
|
{line || '\u00A0'}
|
||||||
|
</span>
|
||||||
))}
|
))}
|
||||||
{itemHandles}
|
{itemHandles}
|
||||||
</div>
|
</div>
|
||||||
|
@ -168,13 +168,45 @@ export function computeStampPreviewStyle(
|
|||||||
const xPts = calcX();
|
const xPts = calcX();
|
||||||
const yPts = calcY();
|
const yPts = calcY();
|
||||||
const xPx = xPts * scaleX;
|
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 widthPx = widthPtsContent * scaleX;
|
||||||
const heightPx = heightPtsContent * scaleY;
|
const heightPx = heightPtsContent * scaleY;
|
||||||
|
|
||||||
const opacity = Math.max(0, Math.min(1, parameters.opacity / 100));
|
const opacity = Math.max(0, Math.min(1, parameters.opacity / 100));
|
||||||
const displayOpacity = opacity;
|
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 {
|
return {
|
||||||
container: {
|
container: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -198,6 +230,7 @@ export function computeStampPreviewStyle(
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
|
alignItems,
|
||||||
cursor: showQuickGrid ? 'default' : 'move',
|
cursor: showQuickGrid ? 'default' : 'move',
|
||||||
pointerEvents: showQuickGrid ? 'none' : 'auto',
|
pointerEvents: showQuickGrid ? 'none' : 'auto',
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ export interface AddStampParameters extends BaseParameters {
|
|||||||
customMargin: 'small' | 'medium' | 'large' | 'x-large';
|
customMargin: 'small' | 'medium' | 'large' | 'x-large';
|
||||||
customColor: string;
|
customColor: string;
|
||||||
pageNumbers: string;
|
pageNumbers: string;
|
||||||
|
_activePill: 'fontSize' | 'rotation' | 'opacity';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultParameters: AddStampParameters = {
|
export const defaultParameters: AddStampParameters = {
|
||||||
@ -30,6 +31,7 @@ export const defaultParameters: AddStampParameters = {
|
|||||||
customMargin: 'medium',
|
customMargin: 'medium',
|
||||||
customColor: '#d3d3d3',
|
customColor: '#d3d3d3',
|
||||||
pageNumbers: '1',
|
pageNumbers: '1',
|
||||||
|
_activePill: 'fontSize',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddStampParametersHook = BaseParametersHook<AddStampParameters>;
|
export type AddStampParametersHook = BaseParametersHook<AddStampParameters>;
|
||||||
|
@ -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<SingleExpansionContextType>({
|
|
||||||
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<string | null>(initialExpandedStep);
|
|
||||||
|
|
||||||
const handleSetExpandedStep = useCallback((stepId: string | null) => {
|
|
||||||
setExpandedStep(stepId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const contextValue: SingleExpansionContextType = {
|
|
||||||
expandedStep,
|
|
||||||
setExpandedStep: handleSetExpandedStep,
|
|
||||||
enabled,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SingleExpansionContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</SingleExpansionContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
@ -5,8 +5,6 @@ import OperationButton from './OperationButton';
|
|||||||
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
||||||
import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle';
|
import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle';
|
||||||
import { StirlingFile } from '../../../types/fileContext';
|
import { StirlingFile } from '../../../types/fileContext';
|
||||||
import { SingleExpansionProvider } from './SingleExpansionContext';
|
|
||||||
import { useSingleExpandController } from './useSingleExpandController';
|
|
||||||
|
|
||||||
export interface FilesStepConfig {
|
export interface FilesStepConfig {
|
||||||
selectedFiles: StirlingFile[];
|
selectedFiles: StirlingFile[];
|
||||||
@ -59,46 +57,38 @@ export interface ToolFlowConfig {
|
|||||||
executeButton?: ExecuteButtonConfig;
|
executeButton?: ExecuteButtonConfig;
|
||||||
review: ReviewStepConfig;
|
review: ReviewStepConfig;
|
||||||
forceStepNumbers?: boolean;
|
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 steps = createToolSteps();
|
||||||
const { onToggle, isCollapsed } = useSingleExpandController({
|
|
||||||
filesVisible: config.files.isVisible !== false,
|
|
||||||
stepVisibilities: config.steps.map(s => s.isVisible),
|
|
||||||
resultsVisible: config.review.isVisible,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm" p="sm">
|
<Stack gap="sm" p="sm" >
|
||||||
|
{/* <Stack gap="sm" p="sm" h="100%" w="100%" style={{ overflow: 'auto' }}> */}
|
||||||
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
|
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
|
||||||
{config.title && <ToolWorkflowTitle {...config.title} />}
|
{config.title && <ToolWorkflowTitle {...config.title} />}
|
||||||
|
|
||||||
{/* Files Step */}
|
{/* Files Step */}
|
||||||
{config.files.isVisible !== false && steps.createFilesStep({
|
{config.files.isVisible !== false && steps.createFilesStep({
|
||||||
selectedFiles: config.files.selectedFiles,
|
selectedFiles: config.files.selectedFiles,
|
||||||
isCollapsed: isCollapsed('files', config.files.isCollapsed),
|
isCollapsed: config.files.isCollapsed,
|
||||||
minFiles: config.files.minFiles,
|
minFiles: config.files.minFiles,
|
||||||
onCollapsedClick: () => onToggle('files', config.files.onCollapsedClick)
|
onCollapsedClick: config.files.onCollapsedClick
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Middle Steps */}
|
{/* Middle Steps */}
|
||||||
{config.steps.map((stepConfig, index) => {
|
{config.steps.map((stepConfig) =>
|
||||||
const stepId = `step-${index}`;
|
steps.create(stepConfig.title, {
|
||||||
return (
|
isVisible: stepConfig.isVisible,
|
||||||
<React.Fragment key={stepId}>
|
isCollapsed: stepConfig.isCollapsed,
|
||||||
{steps.create(stepConfig.title, {
|
onCollapsedClick: stepConfig.onCollapsedClick,
|
||||||
isVisible: stepConfig.isVisible,
|
tooltip: stepConfig.tooltip
|
||||||
isCollapsed: isCollapsed(stepId, stepConfig.isCollapsed),
|
}, stepConfig.content)
|
||||||
onCollapsedClick: () => onToggle(stepId, stepConfig.onCollapsedClick),
|
)}
|
||||||
tooltip: stepConfig.tooltip
|
|
||||||
}, stepConfig.content)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Execute Button */}
|
{/* Execute Button */}
|
||||||
{config.executeButton && config.executeButton.isVisible !== false && (
|
{config.executeButton && config.executeButton.isVisible !== false && (
|
||||||
@ -118,28 +108,9 @@ function ToolFlowContent({ config }: { config: ToolFlowConfig }) {
|
|||||||
operation: config.review.operation,
|
operation: config.review.operation,
|
||||||
title: config.review.title,
|
title: config.review.title,
|
||||||
onFileClick: config.review.onFileClick,
|
onFileClick: config.review.onFileClick,
|
||||||
onUndo: config.review.onUndo,
|
onUndo: config.review.onUndo
|
||||||
isCollapsed: isCollapsed('review', false),
|
|
||||||
onCollapsedClick: () => onToggle('review', undefined)
|
|
||||||
})}
|
})}
|
||||||
</ToolStepProvider>
|
</ToolStepProvider>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolFlowProps extends ToolFlowConfig {}
|
|
||||||
|
|
||||||
export function ToolFlow(props: ToolFlowProps) {
|
|
||||||
return (
|
|
||||||
<SingleExpansionProvider
|
|
||||||
enabled={props.maxOneExpanded ?? false}
|
|
||||||
initialExpandedStep={props.initialExpandedStep ?? null}
|
|
||||||
>
|
|
||||||
<ToolFlowContent config={props} />
|
|
||||||
</SingleExpansionProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createToolFlow(config: ToolFlowConfig) {
|
|
||||||
return <ToolFlow {...config} />;
|
|
||||||
}
|
|
||||||
|
@ -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 };
|
|
||||||
}
|
|
@ -12,14 +12,13 @@ import LocalIcon from "../components/shared/LocalIcon";
|
|||||||
import styles from "../components/tools/addStamp/StampPreview.module.css";
|
import styles from "../components/tools/addStamp/StampPreview.module.css";
|
||||||
import { Tooltip } from "../components/shared/Tooltip";
|
import { Tooltip } from "../components/shared/Tooltip";
|
||||||
import ButtonSelector from "../components/shared/ButtonSelector";
|
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 AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { selectedFiles } = useFileSelection();
|
const { selectedFiles } = useFileSelection();
|
||||||
|
|
||||||
const [collapsedType, setCollapsedType] = useState(false);
|
|
||||||
const [collapsedFormatting, setCollapsedFormatting] = useState(true);
|
|
||||||
const [collapsedPageSelection, setCollapsedPageSelection] = useState(false);
|
|
||||||
const [quickPositionModeSelected, setQuickPositionModeSelected] = useState(false);
|
const [quickPositionModeSelected, setQuickPositionModeSelected] = useState(false);
|
||||||
const [customPositionModeSelected, setCustomPositionModeSelected] = useState(true);
|
const [customPositionModeSelected, setCustomPositionModeSelected] = useState(true);
|
||||||
|
|
||||||
@ -48,14 +47,34 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const hasFiles = selectedFiles.length > 0;
|
const hasFiles = selectedFiles.length > 0;
|
||||||
const hasResults = operation.files.length > 0 || operation.downloadUrl !== null;
|
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<AddStampStep>({
|
||||||
|
noneValue: AddStampStep.NONE,
|
||||||
|
initialStep: AddStampStep.PAGE_SELECTION,
|
||||||
|
stateConditions: {
|
||||||
|
hasFiles,
|
||||||
|
hasResults
|
||||||
|
},
|
||||||
|
afterResults: () => {
|
||||||
|
operation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const getSteps = () => {
|
const getSteps = () => {
|
||||||
const steps: any[] = [];
|
const steps: any[] = [];
|
||||||
|
|
||||||
// Step 1: File settings (page selection)
|
// Step 1: File settings (page selection)
|
||||||
steps.push({
|
steps.push({
|
||||||
title: t("AddStampRequest.pageSelection", "Page Selection"),
|
title: t("AddStampRequest.pageSelection", "Page Selection"),
|
||||||
isCollapsed: hasResults || collapsedPageSelection,
|
isCollapsed: accordion.getCollapsedState(AddStampStep.PAGE_SELECTION),
|
||||||
onCollapsedClick: hasResults ? () => operation.resetResults() : () => setCollapsedPageSelection(!collapsedPageSelection),
|
onCollapsedClick: () => accordion.handleStepToggle(AddStampStep.PAGE_SELECTION),
|
||||||
isVisible: hasFiles || hasResults,
|
isVisible: hasFiles || hasResults,
|
||||||
content: (
|
content: (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
@ -72,8 +91,8 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
// Step 2: Type & Content
|
// Step 2: Type & Content
|
||||||
steps.push({
|
steps.push({
|
||||||
title: t("AddStampRequest.stampType", "Stamp Type"),
|
title: t("AddStampRequest.stampType", "Stamp Type"),
|
||||||
isCollapsed: hasResults ? true : collapsedType,
|
isCollapsed: accordion.getCollapsedState(AddStampStep.STAMP_TYPE),
|
||||||
onCollapsedClick: hasResults ? () => operation.resetResults() : () => setCollapsedType(!collapsedType),
|
onCollapsedClick: () => accordion.handleStepToggle(AddStampStep.STAMP_TYPE),
|
||||||
isVisible: hasFiles || hasResults,
|
isVisible: hasFiles || hasResults,
|
||||||
content: (
|
content: (
|
||||||
<Stack gap="md" justify="space-between" flex={1}>
|
<Stack gap="md" justify="space-between" flex={1}>
|
||||||
@ -161,8 +180,8 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
// Step 3: Formatting & Position
|
// Step 3: Formatting & Position
|
||||||
steps.push({
|
steps.push({
|
||||||
title: t("AddStampRequest.positionAndFormatting", "Position & Formatting"),
|
title: t("AddStampRequest.positionAndFormatting", "Position & Formatting"),
|
||||||
isCollapsed: hasResults ? true : collapsedFormatting,
|
isCollapsed: accordion.getCollapsedState(AddStampStep.POSITION_FORMATTING),
|
||||||
onCollapsedClick: hasResults ? () => operation.resetResults() : () => setCollapsedFormatting(!collapsedFormatting),
|
onCollapsedClick: () => accordion.handleStepToggle(AddStampStep.POSITION_FORMATTING),
|
||||||
isVisible: hasFiles || hasResults,
|
isVisible: hasFiles || hasResults,
|
||||||
content: (
|
content: (
|
||||||
<Stack gap="md" justify="space-between">
|
<Stack gap="md" justify="space-between">
|
||||||
@ -201,27 +220,27 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
<div className="flex justify-between gap-[0.5rem]">
|
<div className="flex justify-between gap-[0.5rem]">
|
||||||
<Tooltip content={t('AddStampRequest.rotation', 'Rotation')} position="top">
|
<Tooltip content={t('AddStampRequest.rotation', 'Rotation')} position="top">
|
||||||
<Button
|
<Button
|
||||||
variant={(params.parameters as any)._activePill === 'rotation' ? 'filled' : 'outline'}
|
variant={params.parameters._activePill === 'rotation' ? 'filled' : 'outline'}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
onClick={() => params.updateParameter('_activePill' as any, 'rotation' as any)}
|
onClick={() => params.updateParameter('_activePill', 'rotation')}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="rotate-right-rounded" width="1.1rem" height="1.1rem" />
|
<LocalIcon icon="rotate-right-rounded" width="1.1rem" height="1.1rem" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={t('AddStampRequest.opacity', 'Opacity')} position="top">
|
<Tooltip content={t('AddStampRequest.opacity', 'Opacity')} position="top">
|
||||||
<Button
|
<Button
|
||||||
variant={(params.parameters as any)._activePill === 'opacity' ? 'filled' : 'outline'}
|
variant={params.parameters._activePill === 'opacity' ? 'filled' : 'outline'}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
onClick={() => params.updateParameter('_activePill' as any, 'opacity' as any)}
|
onClick={() => params.updateParameter('_activePill', 'opacity')}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="opacity" width="1.1rem" height="1.1rem" />
|
<LocalIcon icon="opacity" width="1.1rem" height="1.1rem" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip content={params.parameters.stampType === 'image' ? t('AddStampRequest.imageSize', 'Image Size') : t('AddStampRequest.fontSize', 'Font Size')} position="top">
|
<Tooltip content={params.parameters.stampType === 'image' ? t('AddStampRequest.imageSize', 'Image Size') : t('AddStampRequest.fontSize', 'Font Size')} position="top">
|
||||||
<Button
|
<Button
|
||||||
variant={(params.parameters as any)._activePill === 'fontSize' ? 'filled' : 'outline'}
|
variant={params.parameters._activePill === 'fontSize' ? 'filled' : 'outline'}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
onClick={() => params.updateParameter('_activePill' as any, 'fontSize' as any)}
|
onClick={() => params.updateParameter('_activePill', 'fontSize')}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="zoom-in-map-rounded" width="1.1rem" height="1.1rem" />
|
<LocalIcon icon="zoom-in-map-rounded" width="1.1rem" height="1.1rem" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -229,7 +248,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Single slider bound to selected pill */}
|
{/* Single slider bound to selected pill */}
|
||||||
{(params.parameters as any)._activePill === 'fontSize' && (
|
{params.parameters._activePill === 'fontSize' && (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text className={styles.labelText}>
|
<Text className={styles.labelText}>
|
||||||
{params.parameters.stampType === 'image'
|
{params.parameters.stampType === 'image'
|
||||||
@ -259,7 +278,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{(params.parameters as any)._activePill === 'rotation' && (
|
{params.parameters._activePill === 'rotation' && (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text className={styles.labelText}>{t('AddStampRequest.rotation', 'Rotation')}</Text>
|
<Text className={styles.labelText}>{t('AddStampRequest.rotation', 'Rotation')}</Text>
|
||||||
<Group className={styles.sliderGroup} align="center">
|
<Group className={styles.sliderGroup} align="center">
|
||||||
@ -285,7 +304,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{(params.parameters as any)._activePill === 'opacity' && (
|
{params.parameters._activePill === 'opacity' && (
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text className={styles.labelText}>{t('AddStampRequest.opacity', 'Opacity')}</Text>
|
<Text className={styles.labelText}>{t('AddStampRequest.opacity', 'Opacity')}</Text>
|
||||||
<Group className={styles.sliderGroup} align="center">
|
<Group className={styles.sliderGroup} align="center">
|
||||||
@ -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 */}
|
||||||
<StampPreview
|
<ObscuredOverlay
|
||||||
parameters={params.parameters}
|
obscured={
|
||||||
onParameterChange={params.updateParameter}
|
accordion.currentStep === AddStampStep.POSITION_FORMATTING &&
|
||||||
file={selectedFiles[0] || null}
|
((params.parameters.stampType === 'text' && params.parameters.stampText.trim().length === 0) ||
|
||||||
showQuickGrid={params.parameters.stampType === 'text' ? true : quickPositionModeSelected}
|
(params.parameters.stampType === 'image' && !params.parameters.stampImage))
|
||||||
/>
|
}
|
||||||
|
overlayMessage={
|
||||||
|
<Text size="sm" c="white" fw={600}>
|
||||||
|
{t('AddStampRequest.noStampSelected', 'No stamp selected. Return to Step 3.')}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StampPreview
|
||||||
|
parameters={params.parameters}
|
||||||
|
onParameterChange={params.updateParameter}
|
||||||
|
file={selectedFiles[0] || null}
|
||||||
|
showQuickGrid={params.parameters.stampType === 'text' ? true : quickPositionModeSelected}
|
||||||
|
/>
|
||||||
|
</ObscuredOverlay>
|
||||||
</Stack>
|
</Stack>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -377,9 +409,6 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
onPreviewFile?.(null);
|
onPreviewFile?.(null);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
forceStepNumbers: true,
|
|
||||||
maxOneExpanded: true,
|
|
||||||
initialExpandedStep: "files"
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user