Files
srbhr--resume-matcher/apps/frontend/components/builder/section-header.tsx
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

265 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { ChevronUp, ChevronDown, Trash2, Eye, EyeOff, Pencil, Check, X } from 'lucide-react';
import type { SectionMeta } from '@/components/dashboard/resume-component';
import { useTranslations } from '@/lib/i18n';
interface SectionHeaderProps {
section: SectionMeta;
onRename: (newName: string) => void;
onDelete: () => void;
onMoveUp: () => void;
onMoveDown: () => void;
onToggleVisibility: () => void;
isFirst: boolean;
isLast: boolean;
canDelete: boolean;
children?: React.ReactNode;
}
/**
* SectionHeader Component
*
* Provides controls for section management:
* - Editable display name
* - Move up/down buttons for reordering
* - Delete button with confirmation
* - Visibility toggle
*/
export const SectionHeader: React.FC<SectionHeaderProps> = ({
section,
onRename,
onDelete,
onMoveUp,
onMoveDown,
onToggleVisibility,
isFirst,
isLast,
canDelete,
children,
}) => {
const { t } = useTranslations();
const [isEditing, setIsEditing] = useState(false);
const [editedName, setEditedName] = useState(section.displayName);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const handleStartEdit = () => {
setEditedName(section.displayName);
setIsEditing(true);
};
const handleSaveEdit = () => {
if (editedName.trim()) {
onRename(editedName.trim());
}
setIsEditing(false);
};
const handleCancelEdit = () => {
setEditedName(section.displayName);
setIsEditing(false);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSaveEdit();
} else if (e.key === 'Escape') {
handleCancelEdit();
}
};
const handleDeleteClick = () => {
if (section.isDefault) {
// For default sections, just toggle visibility
onToggleVisibility();
} else {
// For custom sections, show confirmation
setShowDeleteConfirm(true);
}
};
const isPersonalInfo = section.id === 'personalInfo';
const isHidden = !section.isVisible;
return (
<div
className={`space-y-0 border p-6 bg-white shadow-sw-default ${
isHidden ? 'border-dashed border-steel-grey opacity-60' : 'border-black'
}`}
>
{/* Section Header */}
<div className="flex justify-between items-center border-b border-black pb-2 mb-4">
{/* Section Name (editable) */}
<div className="flex items-center gap-2">
{isEditing ? (
<div className="flex items-center gap-1">
<Input
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
onKeyDown={handleKeyDown}
className="h-8 w-48 rounded-none border-black font-serif text-lg font-bold"
autoFocus
/>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-green-700 hover:text-green-800 hover:bg-green-50"
onClick={handleSaveEdit}
aria-label={t('common.save')}
title={t('common.save')}
>
<Check className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-steel-grey hover:text-ink-soft hover:bg-paper-tint"
onClick={handleCancelEdit}
aria-label={t('common.cancel')}
title={t('common.cancel')}
>
<X className="w-4 h-4" />
</Button>
</div>
) : (
<>
<h3 className="font-serif text-xl font-bold">{section.displayName}</h3>
{!isPersonalInfo && (
<Button
variant="ghost"
size="icon"
// Visible 24×24 (matches the small inline pencil aesthetic
// next to the section title), but the touch area is
// extended to 44×44 via -inset-[10px] to meet WCAG 2.5.8.
// The default Button overlay (-inset-1.5) only gives 36×36
// for h-6 buttons; this override adds 4 more px per side.
className="h-6 w-6 text-steel-grey hover:text-ink-soft before:-inset-[10px]"
onClick={handleStartEdit}
aria-label={t('builder.sectionHeader.renameSection')}
title={t('builder.sectionHeader.renameSection')}
>
<Pencil className="w-3 h-3" />
</Button>
)}
{!section.isDefault && (
<span className="font-mono text-[10px] uppercase tracking-wider text-steel-grey bg-paper-tint px-1.5 py-0.5 border border-paper-tint">
{t('builder.sectionHeader.customTag')}
</span>
)}
{isHidden && (
<span className="font-mono text-[10px] uppercase tracking-wider text-orange-600 bg-white px-1.5 py-0.5 border border-orange-500">
{t('builder.sectionHeader.hiddenFromPdfTag')}
</span>
)}
</>
)}
</div>
{/* Section Controls */}
<div className="flex items-center gap-1">
{/* Visibility Toggle. The parent container already applies
opacity-60 when hidden (line 91), which carries the visual
"faded" cue for the hidden state. A conditional text color
here would be redundant — just use steel-grey. */}
{!isPersonalInfo && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-steel-grey"
onClick={onToggleVisibility}
aria-label={
section.isVisible
? t('builder.sectionHeader.hideSection')
: t('builder.sectionHeader.showSection')
}
aria-pressed={!section.isVisible}
title={
section.isVisible
? t('builder.sectionHeader.hideSection')
: t('builder.sectionHeader.showSection')
}
>
{section.isVisible ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</Button>
)}
{/* Move Up */}
{!isPersonalInfo && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-steel-grey hover:text-ink-soft disabled:opacity-30"
onClick={onMoveUp}
disabled={isFirst}
aria-label={t('builder.sectionHeader.moveUp')}
title={t('builder.sectionHeader.moveUp')}
>
<ChevronUp className="w-4 h-4" />
</Button>
)}
{/* Move Down */}
{!isPersonalInfo && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-steel-grey hover:text-ink-soft disabled:opacity-30"
onClick={onMoveDown}
disabled={isLast}
aria-label={t('builder.sectionHeader.moveDown')}
title={t('builder.sectionHeader.moveDown')}
>
<ChevronDown className="w-4 h-4" />
</Button>
)}
{/* Delete / Hide */}
{canDelete && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={handleDeleteClick}
aria-label={
section.isDefault
? section.isVisible
? t('builder.sectionHeader.hideSection')
: t('builder.sectionHeader.showSection')
: t('builder.sectionHeader.deleteSection')
}
title={
section.isDefault
? section.isVisible
? t('builder.sectionHeader.hideSection')
: t('builder.sectionHeader.showSection')
: t('builder.sectionHeader.deleteSection')
}
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
</div>
{/* Section Content */}
{children}
{/* Delete Confirmation Dialog */}
<ConfirmDialog
open={showDeleteConfirm}
onOpenChange={setShowDeleteConfirm}
title={t('builder.sectionHeader.deleteTitle')}
description={t('builder.sectionHeader.deleteDescription', { name: section.displayName })}
confirmLabel={t('common.delete')}
cancelLabel={t('common.cancel')}
variant="danger"
onConfirm={onDelete}
/>
</div>
);
};