Files
documenso/packages/ui/primitives/signature-pad/signature-tabs.tsx
David Nguyen 063fd32f18 feat: add signature configurations (#1710)
Add ability to enable or disable allowed signature types:
- Drawn
- Typed
- Uploaded

**Tabbed style signature dialog**

![image](https://github.com/user-attachments/assets/a816fab6-b071-42a5-bb5c-6d4a2572431e)

**Document settings**

![image](https://github.com/user-attachments/assets/f0c1bff1-6be1-4c87-b384-1666fa25d7a6)

**Team preferences**

![image](https://github.com/user-attachments/assets/8767b05e-1463-4087-8672-f3f43d8b0f2c)

- Add multiselect to select allowed signatures in document and templates
settings tab
- Add multiselect to select allowed signatures in teams preferences
- Removed "Enable typed signatures" from document/template edit page
- Refactored signature pad to use tabs instead of an all in one
signature pad

Added E2E tests to check settings are applied correctly for documents
and templates
2025-03-24 17:13:11 +11:00

148 lines
3.6 KiB
TypeScript

import * as React from 'react';
import { motion } from 'framer-motion';
import { cn } from '../../lib/utils';
interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
defaultValue?: string;
value?: string;
onValueChange?: (value: string) => void;
children: React.ReactNode;
}
interface TabsContextValue {
value: string;
onValueChange: (value: string) => void;
}
const TabsContext = React.createContext<TabsContextValue | undefined>(undefined);
function useTabs() {
const context = React.useContext(TabsContext);
if (!context) {
throw new Error('useTabs must be used within a Tabs provider');
}
return context;
}
export function Tabs({
defaultValue,
value,
onValueChange,
children,
className,
...props
}: TabsProps) {
const [tabValue, setTabValue] = React.useState(defaultValue || '');
const handleValueChange = React.useCallback(
(newValue: string) => {
setTabValue(newValue);
onValueChange?.(newValue);
},
[onValueChange],
);
const contextValue = React.useMemo(
() => ({
value: value !== undefined ? value : tabValue,
onValueChange: handleValueChange,
}),
[value, tabValue, handleValueChange],
);
return (
<TabsContext.Provider value={contextValue}>
<div className={cn('w-full', className)} {...props}>
{children}
</div>
</TabsContext.Provider>
);
}
interface TabsListProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export function TabsList({ children, className, ...props }: TabsListProps) {
return (
<div
className={cn('border-border flex flex-wrap border-b', className)}
role="tabslist"
{...props}
>
{children}
</div>
);
}
interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
value: string;
icon?: React.ReactNode;
children: React.ReactNode;
}
export function TabsTrigger({ value, icon, children, className, ...props }: TabsTriggerProps) {
const { value: selectedValue, onValueChange } = useTabs();
const isSelected = selectedValue === value;
return (
<button
role="tab"
type="button"
aria-selected={isSelected}
data-state={isSelected ? 'active' : 'inactive'}
onClick={() => onValueChange(value)}
className={cn(
'relative flex items-center px-4 py-3 text-sm font-medium transition-all',
'focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
isSelected ? 'text-foreground' : 'text-muted-foreground hover:text-foreground',
className,
)}
{...props}
>
{icon && <span className="flex items-center">{icon}</span>}
{children}
{isSelected && (
<motion.div
layoutId="activeTabIndicator"
className="bg-foreground/40 absolute bottom-0 left-0 h-0.5 w-full rounded-full"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
type: 'spring',
stiffness: 500,
damping: 50,
}}
/>
)}
</button>
);
}
interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
value: string;
children: React.ReactNode;
}
export function TabsContent({ value, children, className, ...props }: TabsContentProps) {
const { value: selectedValue } = useTabs();
const isSelected = selectedValue === value;
if (!isSelected) {
return null;
}
return (
<div
role="tabpanel"
data-state={isSelected ? 'active' : 'inactive'}
className={cn('mt-4', className)}
{...props}
>
{children}
</div>
);
}