mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Feature/v2/watermark (#4215)
Add watermark feature Auto scroll on review --------- Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import React, { useRef } from "react";
|
||||
import { FileButton, Button } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface FileUploadButtonProps {
|
||||
file?: File;
|
||||
onChange: (file: File | null) => void;
|
||||
accept?: string;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
variant?: "outline" | "filled" | "light" | "default" | "subtle" | "gradient";
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const FileUploadButton = ({
|
||||
file,
|
||||
onChange,
|
||||
accept = "*/*",
|
||||
disabled = false,
|
||||
placeholder,
|
||||
variant = "outline",
|
||||
fullWidth = true
|
||||
}: FileUploadButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const resetRef = useRef<() => void>(null);
|
||||
|
||||
const defaultPlaceholder = t('chooseFile', 'Choose File');
|
||||
|
||||
return (
|
||||
<FileButton
|
||||
resetRef={resetRef}
|
||||
onChange={onChange}
|
||||
accept={accept}
|
||||
disabled={disabled}
|
||||
>
|
||||
{(props) => (
|
||||
<Button {...props} variant={variant} fullWidth={fullWidth}>
|
||||
{file ? file.name : (placeholder || defaultPlaceholder)}
|
||||
</Button>
|
||||
)}
|
||||
</FileButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileUploadButton;
|
||||
@@ -2,7 +2,8 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils';
|
||||
import { useTooltipPosition } from '../../hooks/useTooltipPosition';
|
||||
import { TooltipContent, TooltipTip } from './tooltip/TooltipContent';
|
||||
import { TooltipTip } from '../../types/tips';
|
||||
import { TooltipContent } from './tooltip/TooltipContent';
|
||||
import { useSidebarContext } from '../../contexts/SidebarContext';
|
||||
import styles from './tooltip/Tooltip.module.css'
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import React from 'react';
|
||||
import styles from './Tooltip.module.css';
|
||||
|
||||
export interface TooltipTip {
|
||||
title?: string;
|
||||
description?: string;
|
||||
bullets?: string[];
|
||||
body?: React.ReactNode;
|
||||
}
|
||||
import { TooltipTip } from '../../../types/tips';
|
||||
|
||||
interface TooltipContentProps {
|
||||
content?: React.ReactNode;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import { Stack, Checkbox, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||
import NumberInputWithUnit from "../shared/NumberInputWithUnit";
|
||||
|
||||
interface WatermarkFormattingProps {
|
||||
parameters: AddWatermarkParameters;
|
||||
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const WatermarkFormatting = ({ parameters, onParameterChange, disabled = false }: WatermarkFormattingProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Size - single row */}
|
||||
<NumberInputWithUnit
|
||||
label={t('watermark.settings.size', 'Size')}
|
||||
value={parameters.fontSize}
|
||||
onChange={(value) => onParameterChange('fontSize', typeof value === 'number' ? value : 12)}
|
||||
unit={parameters.watermarkType === 'text' ? 'pt' : 'px'}
|
||||
min={1}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{/* Position & Appearance - 2 per row */}
|
||||
<Group grow align="flex-start">
|
||||
<NumberInputWithUnit
|
||||
label={t('watermark.settings.rotation', 'Rotation')}
|
||||
value={parameters.rotation}
|
||||
onChange={(value) => onParameterChange('rotation', typeof value === 'number' ? value : 0)}
|
||||
unit="°"
|
||||
min={-360}
|
||||
max={360}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<NumberInputWithUnit
|
||||
label={t('watermark.settings.opacity', 'Opacity')}
|
||||
value={parameters.opacity}
|
||||
onChange={(value) => onParameterChange('opacity', typeof value === 'number' ? value : 50)}
|
||||
unit="%"
|
||||
min={0}
|
||||
max={100}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Spacing - 2 per row */}
|
||||
<Group grow align="flex-start">
|
||||
<NumberInputWithUnit
|
||||
label={t('watermark.settings.spacing.horizontal', 'Horizontal Spacing')}
|
||||
value={parameters.widthSpacer}
|
||||
onChange={(value) => onParameterChange('widthSpacer', typeof value === 'number' ? value : 50)}
|
||||
unit="px"
|
||||
min={0}
|
||||
max={200}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<NumberInputWithUnit
|
||||
label={t('watermark.settings.spacing.vertical', 'Vertical Spacing')}
|
||||
value={parameters.heightSpacer}
|
||||
onChange={(value) => onParameterChange('heightSpacer', typeof value === 'number' ? value : 50)}
|
||||
unit="px"
|
||||
min={0}
|
||||
max={200}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Advanced Options */}
|
||||
<Checkbox
|
||||
label={t('watermark.settings.convertToImage', 'Flatten PDF pages to images')}
|
||||
checked={parameters.convertPDFToImage}
|
||||
onChange={(event) => onParameterChange('convertPDFToImage', event.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default WatermarkFormatting;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import { Stack } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||
import FileUploadButton from "../../shared/FileUploadButton";
|
||||
|
||||
interface WatermarkImageFileProps {
|
||||
parameters: AddWatermarkParameters;
|
||||
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const WatermarkImageFile = ({ parameters, onParameterChange, disabled = false }: WatermarkImageFileProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<FileUploadButton
|
||||
file={parameters.watermarkImage}
|
||||
onChange={(file) => onParameterChange('watermarkImage', file)}
|
||||
accept="image/*"
|
||||
disabled={disabled}
|
||||
placeholder={t('watermark.settings.image.choose', 'Choose Image')}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default WatermarkImageFile;
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
import { Stack, Text, NumberInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||
|
||||
interface WatermarkStyleSettingsProps {
|
||||
parameters: AddWatermarkParameters;
|
||||
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = false }: WatermarkStyleSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Appearance Settings */}
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>{t('watermark.settings.rotation', 'Rotation (degrees)')}</Text>
|
||||
<NumberInput
|
||||
value={parameters.rotation}
|
||||
onChange={(value) => onParameterChange('rotation', value || 0)}
|
||||
min={-360}
|
||||
max={360}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Text size="sm" fw={500}>{t('watermark.settings.opacity', 'Opacity (%)')}</Text>
|
||||
<NumberInput
|
||||
value={parameters.opacity}
|
||||
onChange={(value) => onParameterChange('opacity', value || 50)}
|
||||
min={0}
|
||||
max={100}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* Spacing Settings */}
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>{t('watermark.settings.spacing.width', 'Width Spacing')}</Text>
|
||||
<NumberInput
|
||||
value={parameters.widthSpacer}
|
||||
onChange={(value) => onParameterChange('widthSpacer', value || 50)}
|
||||
min={0}
|
||||
max={200}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Text size="sm" fw={500}>{t('watermark.settings.spacing.height', 'Height Spacing')}</Text>
|
||||
<NumberInput
|
||||
value={parameters.heightSpacer}
|
||||
onChange={(value) => onParameterChange('heightSpacer', value || 50)}
|
||||
min={0}
|
||||
max={200}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default WatermarkStyleSettings;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import { Stack, Text, Select, ColorInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||
import { alphabetOptions } from "../../../constants/addWatermarkConstants";
|
||||
|
||||
interface WatermarkTextStyleProps {
|
||||
parameters: AddWatermarkParameters;
|
||||
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const WatermarkTextStyle = ({ parameters, onParameterChange, disabled = false }: WatermarkTextStyleProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={500}>
|
||||
{t("watermark.settings.color", "Colour")}
|
||||
</Text>
|
||||
<ColorInput
|
||||
value={parameters.customColor}
|
||||
onChange={(value) => onParameterChange("customColor", value)}
|
||||
disabled={disabled}
|
||||
format="hex"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={500}>
|
||||
{t("watermark.settings.alphabet", "Alphabet")}
|
||||
</Text>
|
||||
<Select
|
||||
value={parameters.alphabet}
|
||||
onChange={(value) => value && onParameterChange("alphabet", value)}
|
||||
data={alphabetOptions}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default WatermarkTextStyle;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { Button, Stack, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface WatermarkTypeSettingsProps {
|
||||
watermarkType?: 'text' | 'image';
|
||||
onWatermarkTypeChange: (type: 'text' | 'image') => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled = false }: WatermarkTypeSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<Button
|
||||
variant={watermarkType === 'text' ? 'filled' : 'outline'}
|
||||
color={watermarkType === 'text' ? 'blue' : 'gray'}
|
||||
onClick={() => onWatermarkTypeChange('text')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
{t('watermark.watermarkType.text', 'Text')}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant={watermarkType === 'image' ? 'filled' : 'outline'}
|
||||
color={watermarkType === 'image' ? 'blue' : 'gray'}
|
||||
onClick={() => onWatermarkTypeChange('image')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
{t('watermark.watermarkType.image', 'Image')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default WatermarkTypeSettings;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { Stack, Text, TextInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||
import { removeEmojis } from "../../../utils/textUtils";
|
||||
|
||||
interface WatermarkWordingProps {
|
||||
parameters: AddWatermarkParameters;
|
||||
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const WatermarkWording = ({ parameters, onParameterChange, disabled = false }: WatermarkWordingProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
const filteredValue = removeEmojis(value);
|
||||
onParameterChange('watermarkText', filteredValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
placeholder={t('watermark.settings.text.placeholder', 'Enter watermark text')}
|
||||
value={parameters.watermarkText}
|
||||
onChange={handleTextChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default WatermarkWording;
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Stack, Text, NumberInput } from "@mantine/core";
|
||||
|
||||
interface NumberInputWithUnitProps {
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (value: number | string) => void;
|
||||
unit: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const NumberInputWithUnit = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
unit,
|
||||
min,
|
||||
max,
|
||||
disabled = false
|
||||
}: NumberInputWithUnitProps) => {
|
||||
const [localValue, setLocalValue] = useState<number | string>(value);
|
||||
|
||||
// Sync local value when external value changes
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleBlur = () => {
|
||||
onChange(localValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="xs" style={{ flex: 1 }}>
|
||||
<Text size="xs" fw={500} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{label}
|
||||
</Text>
|
||||
<NumberInput
|
||||
value={localValue}
|
||||
onChange={setLocalValue}
|
||||
onBlur={handleBlur}
|
||||
min={min}
|
||||
max={max}
|
||||
disabled={disabled}
|
||||
rightSection={
|
||||
<Text size="sm" c="dimmed" pr="sm">
|
||||
{unit}
|
||||
</Text>
|
||||
}
|
||||
rightSectionWidth={unit.length * 8 + 20} // Dynamic width based on unit length
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default NumberInputWithUnit;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Button, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
@@ -14,24 +14,32 @@ export interface ReviewToolStepProps<TParams = unknown> {
|
||||
onFileClick?: (file: File) => void;
|
||||
}
|
||||
|
||||
export function createReviewToolStep<TParams = unknown>(
|
||||
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
|
||||
props: ReviewToolStepProps<TParams>
|
||||
): React.ReactElement {
|
||||
function ReviewStepContent<TParams = unknown>({ operation, onFileClick }: { operation: ToolOperationHook<TParams>; onFileClick?: (file: File) => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { operation } = props;
|
||||
const stepRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const previewFiles = operation.files?.map((file, index) => ({
|
||||
file,
|
||||
thumbnail: operation.thumbnails[index]
|
||||
})) || [];
|
||||
|
||||
return createStep(t("review", "Review"), {
|
||||
isVisible: props.isVisible,
|
||||
_excludeFromCount: true,
|
||||
_noPadding: true
|
||||
}, (
|
||||
<Stack gap="sm" >
|
||||
// Auto-scroll to bottom when content appears
|
||||
useEffect(() => {
|
||||
if (stepRef.current && (previewFiles.length > 0 || operation.downloadUrl || operation.errorMessage)) {
|
||||
const scrollableContainer = stepRef.current.closest('[style*="overflow: auto"]') as HTMLElement;
|
||||
if (scrollableContainer) {
|
||||
setTimeout(() => {
|
||||
scrollableContainer.scrollTo({
|
||||
top: scrollableContainer.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 100); // Small delay to ensure content is rendered
|
||||
}
|
||||
}
|
||||
}, [previewFiles.length, operation.downloadUrl, operation.errorMessage]);
|
||||
|
||||
return (
|
||||
<Stack gap="sm" ref={stepRef}>
|
||||
<ErrorNotification
|
||||
error={operation.errorMessage}
|
||||
onClose={operation.clearError}
|
||||
@@ -40,7 +48,7 @@ export function createReviewToolStep<TParams = unknown>(
|
||||
{previewFiles.length > 0 && (
|
||||
<ResultsPreview
|
||||
files={previewFiles}
|
||||
onFileClick={props.onFileClick}
|
||||
onFileClick={onFileClick}
|
||||
isGeneratingThumbnails={operation.isGeneratingThumbnails}
|
||||
/>
|
||||
)}
|
||||
@@ -61,5 +69,23 @@ export function createReviewToolStep<TParams = unknown>(
|
||||
|
||||
<SuggestedToolsSection />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function createReviewToolStep<TParams = unknown>(
|
||||
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
|
||||
props: ReviewToolStepProps<TParams>
|
||||
): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return createStep(t("review", "Review"), {
|
||||
isVisible: props.isVisible,
|
||||
_excludeFromCount: true,
|
||||
_noPadding: true
|
||||
}, (
|
||||
<ReviewStepContent
|
||||
operation={props.operation}
|
||||
onFileClick={props.onFileClick}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import { Text, Stack, Box, Flex, Divider } from '@mantine/core';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import { Tooltip } from '../../shared/Tooltip';
|
||||
import { TooltipTip } from '../../shared/tooltip/TooltipContent';
|
||||
import { TooltipTip } from '../../../types/tips';
|
||||
import { createFilesToolStep, FilesToolStepProps } from './FilesToolStep';
|
||||
import { createReviewToolStep, ReviewToolStepProps } from './ReviewToolStep';
|
||||
|
||||
interface ToolStepContextType {
|
||||
visibleStepCount: number;
|
||||
forceStepNumbers?: boolean;
|
||||
}
|
||||
|
||||
const ToolStepContext = createContext<ToolStepContextType | null>(null);
|
||||
@@ -82,10 +83,11 @@ const ToolStep = ({
|
||||
|
||||
const parent = useContext(ToolStepContext);
|
||||
|
||||
// Auto-detect if we should show numbers based on sibling count
|
||||
// Auto-detect if we should show numbers based on sibling count or force option
|
||||
const shouldShowNumber = useMemo(() => {
|
||||
if (showNumber !== undefined) return showNumber;
|
||||
return parent ? parent.visibleStepCount >= 3 : false;
|
||||
if (showNumber !== undefined) return showNumber; // Individual step override
|
||||
if (parent?.forceStepNumbers) return true; // Flow-level force
|
||||
return parent ? parent.visibleStepCount >= 3 : false; // Auto-detect
|
||||
}, [showNumber, parent]);
|
||||
|
||||
const stepNumber = _stepNumber;
|
||||
@@ -196,7 +198,7 @@ export function createToolSteps() {
|
||||
}
|
||||
|
||||
// Context provider wrapper for tools using the factory
|
||||
export function ToolStepProvider({ children }: { children: React.ReactNode }) {
|
||||
export function ToolStepProvider({ children, forceStepNumbers }: { children: React.ReactNode; forceStepNumbers?: boolean }) {
|
||||
// Count visible steps from children that are ToolStep elements
|
||||
const visibleStepCount = useMemo(() => {
|
||||
let count = 0;
|
||||
@@ -212,8 +214,9 @@ export function ToolStepProvider({ children }: { children: React.ReactNode }) {
|
||||
}, [children]);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
visibleStepCount
|
||||
}), [visibleStepCount]);
|
||||
visibleStepCount,
|
||||
forceStepNumbers
|
||||
}), [visibleStepCount, forceStepNumbers]);
|
||||
|
||||
return (
|
||||
<ToolStepContext.Provider value={contextValue}>
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface ToolFlowConfig {
|
||||
steps: MiddleStepConfig[];
|
||||
executeButton?: ExecuteButtonConfig;
|
||||
review: ReviewStepConfig;
|
||||
forceStepNumbers?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +61,7 @@ export function createToolFlow(config: ToolFlowConfig) {
|
||||
|
||||
return (
|
||||
<Stack gap="sm" p="sm" h="95vh" w="100%" style={{ overflow: 'auto' }}>
|
||||
<ToolStepProvider>
|
||||
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
|
||||
{/* Files Step */}
|
||||
{steps.createFilesStep({
|
||||
selectedFiles: config.files.selectedFiles,
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent, TooltipTip } from '../../types/tips';
|
||||
|
||||
// Shared tooltip content to reduce duplication
|
||||
const useSharedWatermarkContent = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const languageSupportTip: TooltipTip = {
|
||||
title: t("watermark.tooltip.language.title", "Language Support"),
|
||||
description: t("watermark.tooltip.language.text", "Choose the appropriate language setting to ensure proper font rendering for your text.")
|
||||
};
|
||||
|
||||
const appearanceTip: TooltipTip = {
|
||||
title: t("watermark.tooltip.appearance.title", "Appearance Settings"),
|
||||
description: t("watermark.tooltip.appearance.text", "Control how your watermark looks and blends with the document."),
|
||||
bullets: [
|
||||
t("watermark.tooltip.appearance.bullet1", "Rotation: -360° to 360° for angled watermarks"),
|
||||
t("watermark.tooltip.appearance.bullet2", "Opacity: 0-100% for transparency control"),
|
||||
t("watermark.tooltip.appearance.bullet3", "Lower opacity creates subtle watermarks")
|
||||
]
|
||||
};
|
||||
|
||||
const spacingTip: TooltipTip = {
|
||||
title: t("watermark.tooltip.spacing.title", "Spacing Control"),
|
||||
description: t("watermark.tooltip.spacing.text", "Adjust the spacing between repeated watermarks across the page."),
|
||||
bullets: [
|
||||
t("watermark.tooltip.spacing.bullet1", "Width spacing: Horizontal distance between watermarks"),
|
||||
t("watermark.tooltip.spacing.bullet2", "Height spacing: Vertical distance between watermarks"),
|
||||
t("watermark.tooltip.spacing.bullet3", "Higher values create more spread out patterns")
|
||||
]
|
||||
};
|
||||
|
||||
return { languageSupportTip, appearanceTip, spacingTip };
|
||||
};
|
||||
|
||||
export const useWatermarkTypeTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("watermark.tooltip.type.header.title", "Watermark Type Selection")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("watermark.tooltip.type.description.title", "Choose Your Watermark"),
|
||||
description: t("watermark.tooltip.type.description.text", "Select between text or image watermarks based on your needs.")
|
||||
},
|
||||
{
|
||||
title: t("watermark.tooltip.type.text.title", "Text Watermarks"),
|
||||
description: t("watermark.tooltip.type.text.text", "Perfect for adding copyright notices, company names, or confidentiality labels. Supports multiple languages and custom colors."),
|
||||
bullets: [
|
||||
t("watermark.tooltip.type.text.bullet1", "Customizable fonts and languages"),
|
||||
t("watermark.tooltip.type.text.bullet2", "Adjustable colors and transparency"),
|
||||
t("watermark.tooltip.type.text.bullet3", "Ideal for legal or branding text")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("watermark.tooltip.type.image.title", "Image Watermarks"),
|
||||
description: t("watermark.tooltip.type.image.text", "Use logos, stamps, or any image as a watermark. Great for branding and visual identification."),
|
||||
bullets: [
|
||||
t("watermark.tooltip.type.image.bullet1", "Upload any image format"),
|
||||
t("watermark.tooltip.type.image.bullet2", "Maintains image quality"),
|
||||
t("watermark.tooltip.type.image.bullet3", "Perfect for logos and stamps")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const useWatermarkWordingTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("watermark.tooltip.wording.header.title", "Text Content")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("watermark.tooltip.wording.text.title", "Watermark Text"),
|
||||
description: t("watermark.tooltip.wording.text.text", "Enter the text that will appear as your watermark across the document."),
|
||||
bullets: [
|
||||
t("watermark.tooltip.wording.text.bullet1", "Keep it concise for better readability"),
|
||||
t("watermark.tooltip.wording.text.bullet2", "Common examples: 'CONFIDENTIAL', 'DRAFT', company name"),
|
||||
t("watermark.tooltip.wording.text.bullet3", "Emoji characters are not supported and will be filtered out")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
export const useWatermarkTextStyleTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
const { languageSupportTip } = useSharedWatermarkContent();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("watermark.tooltip.textStyle.header.title", "Text Style")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("watermark.tooltip.textStyle.color.title", "Color Selection"),
|
||||
description: t("watermark.tooltip.textStyle.color.text", "Choose a color that provides good contrast with your document content."),
|
||||
bullets: [
|
||||
t("watermark.tooltip.textStyle.color.bullet1", "Light gray (#d3d3d3) for subtle watermarks"),
|
||||
t("watermark.tooltip.textStyle.color.bullet2", "Black or dark colors for high contrast"),
|
||||
t("watermark.tooltip.textStyle.color.bullet3", "Custom colors for branding purposes")
|
||||
]
|
||||
},
|
||||
languageSupportTip
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
export const useWatermarkFileTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("watermark.tooltip.file.header.title", "Image Upload")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("watermark.tooltip.file.upload.title", "Image Selection"),
|
||||
description: t("watermark.tooltip.file.upload.text", "Upload an image file to use as your watermark."),
|
||||
bullets: [
|
||||
t("watermark.tooltip.file.upload.bullet1", "Supports common formats: PNG, JPG, GIF, BMP"),
|
||||
t("watermark.tooltip.file.upload.bullet2", "PNG with transparency works best"),
|
||||
t("watermark.tooltip.file.upload.bullet3", "Higher resolution images maintain quality better")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("watermark.tooltip.file.recommendations.title", "Best Practices"),
|
||||
description: t("watermark.tooltip.file.recommendations.text", "Tips for optimal image watermark results."),
|
||||
bullets: [
|
||||
t("watermark.tooltip.file.recommendations.bullet1", "Use logos or stamps with transparent backgrounds"),
|
||||
t("watermark.tooltip.file.recommendations.bullet2", "Simple designs work better than complex images"),
|
||||
t("watermark.tooltip.file.recommendations.bullet3", "Consider the final document size when choosing resolution")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
export const useWatermarkFormattingTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
const { appearanceTip, spacingTip } = useSharedWatermarkContent();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("watermark.tooltip.formatting.header.title", "Formatting & Layout")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("watermark.tooltip.formatting.size.title", "Size Control"),
|
||||
description: t("watermark.tooltip.formatting.size.text", "Adjust the size of your watermark (text or image)."),
|
||||
bullets: [
|
||||
t("watermark.tooltip.formatting.size.bullet1", "Larger sizes create more prominent watermarks")
|
||||
]
|
||||
},
|
||||
appearanceTip,
|
||||
spacingTip,
|
||||
{
|
||||
title: t("watermark.tooltip.formatting.security.title", "Security Option"),
|
||||
description: t("watermark.tooltip.formatting.security.text", "Convert the final PDF to an image-based format for enhanced security."),
|
||||
bullets: [
|
||||
t("watermark.tooltip.formatting.security.bullet1", "Prevents text selection and copying"),
|
||||
t("watermark.tooltip.formatting.security.bullet2", "Makes watermarks harder to remove"),
|
||||
t("watermark.tooltip.formatting.security.bullet3", "Results in larger file sizes"),
|
||||
t("watermark.tooltip.formatting.security.bullet4", "Best for sensitive or copyrighted content")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user