mirror of
https://github.com/documenso/documenso.git
synced 2026-07-10 21:15:15 +10:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d6947640a |
+2
-1
@@ -240,9 +240,10 @@ export const DocumentSigningPageViewV2 = () => {
|
||||
{/* Horizontal envelope item selector */}
|
||||
{envelopeItems.length > 1 && (
|
||||
<div className="flex h-fit space-x-2 overflow-x-auto p-2 pt-4 sm:p-4">
|
||||
{envelopeItems.map((doc) => (
|
||||
{envelopeItems.map((doc, i) => (
|
||||
<EnvelopeItemSelector
|
||||
key={doc.id}
|
||||
number={i + 1}
|
||||
primaryText={doc.title}
|
||||
secondaryText={
|
||||
<Plural
|
||||
|
||||
@@ -21,6 +21,7 @@ import { getEnvelopeItemPermissions } from '@documenso/lib/utils/envelope';
|
||||
import { getOverlappingFieldPairs } from '@documenso/lib/utils/fields-overlap';
|
||||
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
|
||||
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Separator } from '@documenso/ui/primitives/separator';
|
||||
@@ -212,19 +213,26 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
editorConfig.envelopeItems.allowReplace &&
|
||||
envelopeItemPermissions.canFileBeChanged
|
||||
? (item) => (
|
||||
<EnvelopeItemEditDialog
|
||||
envelopeItem={item}
|
||||
allowConfigureTitle={editorConfig.envelopeItems?.allowConfigureTitle ?? false}
|
||||
trigger={
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-5 w-5 flex-shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground opacity-0 transition-opacity duration-150 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover:opacity-100"
|
||||
data-testid={`envelope-item-edit-button-${item.id}`}
|
||||
>
|
||||
<PencilIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<div className="relative flex h-5 w-5 flex-shrink-0 items-center justify-center">
|
||||
<div
|
||||
className={cn('h-2 w-2 rounded-full transition-opacity duration-150 group-hover:opacity-0', {
|
||||
'bg-primary': currentEnvelopeItem?.id === item.id,
|
||||
})}
|
||||
/>
|
||||
<EnvelopeItemEditDialog
|
||||
envelopeItem={item}
|
||||
allowConfigureTitle={editorConfig.envelopeItems?.allowConfigureTitle ?? false}
|
||||
trigger={
|
||||
<span
|
||||
className="absolute inset-0 flex cursor-pointer items-center justify-center opacity-0 transition-opacity duration-150 group-hover:opacity-100"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
data-testid={`envelope-item-edit-button-${item.id}`}
|
||||
>
|
||||
<PencilIcon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import type { MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
CheckIcon,
|
||||
CopyPlusIcon,
|
||||
DownloadCloudIcon,
|
||||
EyeIcon,
|
||||
@@ -229,101 +230,125 @@ export const EnvelopeEditor = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-4">
|
||||
<h3 className="flex items-end justify-between font-semibold text-foreground text-sm">
|
||||
<h3 className="flex items-baseline justify-between font-semibold text-foreground text-sm tracking-tight">
|
||||
{isDocument ? <Trans>Document Editor</Trans> : <Trans>Template Editor</Trans>}
|
||||
|
||||
<span className="ml-2 rounded border bg-muted/50 px-2 py-0.5 text-muted-foreground text-xs">
|
||||
<span className="text-muted-foreground text-xs tabular-nums">
|
||||
<Trans context="The step counter">
|
||||
Step {currentStepData.order}/{envelopeEditorSteps.length}
|
||||
</Trans>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<div className="relative my-4 h-[4px] rounded-md bg-muted">
|
||||
<motion.div
|
||||
layout="size"
|
||||
layoutId="document-flow-container-step"
|
||||
className="absolute inset-y-0 left-0 bg-primary"
|
||||
style={{
|
||||
width: `${(100 / envelopeEditorSteps.length) * (currentStepData.order ?? 0)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn('space-y-3', {
|
||||
'px-4': !minimizeLeftSidebar,
|
||||
'mt-4 flex flex-col items-center': minimizeLeftSidebar,
|
||||
<nav
|
||||
className={cn('flex flex-col', {
|
||||
'mt-5 px-4': !minimizeLeftSidebar,
|
||||
'mt-4 items-center': minimizeLeftSidebar,
|
||||
})}
|
||||
>
|
||||
{envelopeEditorSteps.map((step) => {
|
||||
{envelopeEditorSteps.map((step, index) => {
|
||||
const Icon = step.icon;
|
||||
const isActive = searchParamsStep === step.id;
|
||||
const isCompleted = (currentStepData.order ?? 0) > step.order;
|
||||
const isLast = index === envelopeEditorSteps.length - 1;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={step.id}
|
||||
data-testid={`envelope-editor-step-${step.id}`}
|
||||
type="button"
|
||||
className={cn(
|
||||
`cursor-pointer rounded-lg text-left transition-colors ${
|
||||
isActive
|
||||
? 'border border-green-200 bg-green-50 dark:border-green-500/20 dark:bg-green-500/10'
|
||||
: 'border border-gray-200 hover:bg-gray-50 dark:border-gray-400/20 dark:hover:bg-gray-400/10'
|
||||
}`,
|
||||
{
|
||||
'p-3': !minimizeLeftSidebar,
|
||||
},
|
||||
)}
|
||||
title={minimizeLeftSidebar ? t(step.title) : undefined}
|
||||
aria-current={isActive ? 'step' : undefined}
|
||||
className="group flex cursor-pointer rounded-lg text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
||||
onClick={() => void navigateToStep(step.id as EnvelopeEditorStep)}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={`rounded border p-2 ${
|
||||
isActive
|
||||
? 'border-green-200 bg-green-50 dark:border-green-500/20 dark:bg-green-500/10'
|
||||
: 'border-gray-100 bg-gray-100 dark:border-gray-400/20 dark:bg-gray-400/10'
|
||||
}`}
|
||||
className={cn(
|
||||
'flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full border transition-[color,border-color,background-color,box-shadow] duration-150',
|
||||
{
|
||||
'border-primary bg-primary text-primary-foreground': isCompleted,
|
||||
'border-primary bg-primary/10 text-documenso-700 ring-4 ring-primary/10 dark:text-primary':
|
||||
isActive,
|
||||
'border-border bg-background text-muted-foreground group-hover:border-muted-foreground/40 group-hover:text-foreground':
|
||||
!isActive && !isCompleted,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Icon className={`h-4 w-4 ${isActive ? 'text-green-600' : 'text-gray-600'}`} />
|
||||
{isCompleted ? (
|
||||
<motion.span
|
||||
className="flex"
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
||||
>
|
||||
<CheckIcon className="h-3.5 w-3.5" />
|
||||
</motion.span>
|
||||
) : (
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!minimizeLeftSidebar && (
|
||||
<div>
|
||||
<div
|
||||
className={`font-medium text-sm ${
|
||||
isActive
|
||||
? 'text-green-900 dark:text-green-400'
|
||||
: 'text-foreground dark:text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{t(step.title)}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">{t(step.description)}</div>
|
||||
{!isLast && (
|
||||
<div className="relative my-1 min-h-4 w-px flex-1 overflow-hidden bg-border">
|
||||
<motion.div
|
||||
className="absolute inset-x-0 top-0 bg-primary"
|
||||
initial={false}
|
||||
animate={{ height: isCompleted ? '100%' : '0%' }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!minimizeLeftSidebar && (
|
||||
<div className={cn('ml-3 min-w-0 pt-1', !isLast && 'pb-5')}>
|
||||
<div
|
||||
className={cn('font-medium text-sm transition-colors duration-150', {
|
||||
'text-documenso-800 dark:text-primary': isActive,
|
||||
'text-foreground': isCompleted,
|
||||
'text-muted-foreground group-hover:text-foreground': !isActive && !isCompleted,
|
||||
})}
|
||||
>
|
||||
{t(step.title)}
|
||||
</div>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
className="overflow-hidden"
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.15, ease: 'easeOut' }}
|
||||
>
|
||||
<div className="mt-0.5 text-muted-foreground text-xs">{t(step.description)}</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<Separator
|
||||
className={cn('my-6', {
|
||||
className={cn('my-5', {
|
||||
'mx-auto mb-4 w-4/5': minimizeLeftSidebar,
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Quick Actions. */}
|
||||
<div
|
||||
className={cn('space-y-3 px-4 [&_.lucide]:text-muted-foreground', {
|
||||
className={cn('space-y-1 px-4 [&_.lucide]:text-muted-foreground', {
|
||||
'px-2': minimizeLeftSidebar,
|
||||
})}
|
||||
>
|
||||
{!minimizeLeftSidebar && (
|
||||
<h4 className="font-semibold text-foreground text-sm">
|
||||
<h4 className="mb-2 px-3 font-medium text-muted-foreground text-xs uppercase tracking-wider">
|
||||
<Trans>Quick Actions</Trans>
|
||||
</h4>
|
||||
)}
|
||||
@@ -467,7 +492,7 @@ export const EnvelopeEditor = () => {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start"
|
||||
className="w-full justify-start hover:bg-destructive/10 hover:text-destructive hover:[&_.lucide]:text-destructive"
|
||||
title={t(msg`Delete Envelope`)}
|
||||
>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
@@ -493,13 +518,14 @@ export const EnvelopeEditor = () => {
|
||||
{/* Footer of left sidebar. */}
|
||||
{!editorConfig.embedded && (
|
||||
<div
|
||||
className={cn('mt-auto px-4', {
|
||||
className={cn('mt-auto border-border border-t px-4 pt-3', {
|
||||
'px-2': minimizeLeftSidebar,
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', {
|
||||
size="sm"
|
||||
className={cn('w-full justify-start text-muted-foreground hover:text-foreground', {
|
||||
'flex items-center justify-center': minimizeLeftSidebar,
|
||||
})}
|
||||
asChild
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Plural } from '@lingui/react/macro';
|
||||
import { FileTextIcon } from 'lucide-react';
|
||||
|
||||
type EnvelopeItemSelectorProps = {
|
||||
number: number;
|
||||
primaryText: React.ReactNode;
|
||||
secondaryText: React.ReactNode;
|
||||
isSelected: boolean;
|
||||
@@ -12,6 +12,7 @@ type EnvelopeItemSelectorProps = {
|
||||
};
|
||||
|
||||
export const EnvelopeItemSelector = ({
|
||||
number,
|
||||
primaryText,
|
||||
secondaryText,
|
||||
isSelected,
|
||||
@@ -19,42 +20,34 @@ export const EnvelopeItemSelector = ({
|
||||
actionSlot,
|
||||
}: EnvelopeItemSelectorProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group relative flex h-9 max-w-64 flex-shrink-0 items-center rounded-md border transition-colors',
|
||||
<button
|
||||
title={typeof primaryText === 'string' ? primaryText : undefined}
|
||||
className={`group flex h-fit max-w-72 flex-shrink-0 cursor-pointer items-center space-x-3 rounded-lg border px-4 py-3 transition-colors ${
|
||||
isSelected
|
||||
? 'border-border bg-background text-foreground shadow-sm'
|
||||
: 'border-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground',
|
||||
)}
|
||||
? 'border-primary/40 bg-primary/10 text-documenso-800 dark:text-primary'
|
||||
: 'border-border bg-muted/50 hover:bg-muted/70'
|
||||
}`}
|
||||
{...buttonProps}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
title={typeof primaryText === 'string' ? primaryText : undefined}
|
||||
aria-pressed={isSelected}
|
||||
className={cn(
|
||||
'flex h-full min-w-0 cursor-pointer items-center gap-2 rounded-md pl-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||
actionSlot ? 'pr-1' : 'pr-3',
|
||||
)}
|
||||
{...buttonProps}
|
||||
<div
|
||||
className={`flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full font-medium text-xs ${
|
||||
isSelected ? 'bg-primary/20 text-documenso-700 dark:text-primary' : 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
<FileTextIcon className="h-4 w-4 flex-shrink-0 opacity-60" />
|
||||
<span className="truncate font-medium text-sm">{primaryText}</span>
|
||||
{secondaryText ? (
|
||||
<span
|
||||
className={cn(
|
||||
'flex-shrink-0 rounded-full px-1.5 py-px text-xs tabular-nums',
|
||||
isSelected
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-400/20 dark:text-green-400'
|
||||
: 'bg-muted text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{secondaryText}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
{actionSlot && <div className="flex flex-shrink-0 items-center pr-2">{actionSlot}</div>}
|
||||
</div>
|
||||
{number}
|
||||
</div>
|
||||
<div className="min-w-0 text-left">
|
||||
<div className="truncate font-medium text-sm">{primaryText}</div>
|
||||
<div className="text-muted-foreground text-xs">{secondaryText}</div>
|
||||
</div>
|
||||
{actionSlot ?? (
|
||||
<div
|
||||
className={cn('h-2 w-2 flex-shrink-0 rounded-full', {
|
||||
'bg-primary': isSelected,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -74,10 +67,11 @@ export const EnvelopeRendererFileSelector = ({
|
||||
const { envelopeItems, currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||
|
||||
return (
|
||||
<div className={cn('scrollbar-hidden flex h-fit flex-shrink-0 gap-1.5 overflow-x-auto py-3', className)}>
|
||||
{envelopeItems.map((doc) => (
|
||||
<div className={cn('scrollbar-hidden flex h-fit flex-shrink-0 space-x-2 overflow-x-auto p-4', className)}>
|
||||
{envelopeItems.map((doc, i) => (
|
||||
<EnvelopeItemSelector
|
||||
key={doc.id}
|
||||
number={i + 1}
|
||||
primaryText={doc.title}
|
||||
secondaryText={
|
||||
secondaryOverride ?? (
|
||||
|
||||
Reference in New Issue
Block a user