- EntityTable: replace flat rows with grouped sections (label + rows), each group gets its own section header for visual clarity - EntityDetailPanel: add favourite/frequent toggle star button in header, add multiline flag for notes with whitespace-pre-wrap - CopyableField: use inline-flex to keep copy icon close to text - PeoplePage: compute initials from first_name/last_name (not nickname), build row groups from active filters, add toggle favourite mutation - LocationsPage: build row groups from active filters, add toggle frequent mutation, pass favourite props to detail panel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
37 lines
1.2 KiB
TypeScript
37 lines
1.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { Copy, Check, type LucideIcon } from 'lucide-react';
|
|
|
|
interface CopyableFieldProps {
|
|
value: string;
|
|
icon?: LucideIcon;
|
|
label?: string;
|
|
}
|
|
|
|
export default function CopyableField({ value, icon: Icon, label }: CopyableFieldProps) {
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const handleCopy = () => {
|
|
navigator.clipboard.writeText(value).then(() => {
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 1500);
|
|
}).catch(() => {
|
|
// Clipboard API can fail in non-secure contexts or when permission is denied
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="group inline-flex items-center gap-2 max-w-full">
|
|
{Icon && <Icon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />}
|
|
<span className="text-sm truncate">{value}</span>
|
|
<button
|
|
type="button"
|
|
onClick={handleCopy}
|
|
aria-label={`Copy ${label || value}`}
|
|
className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 p-0.5 rounded text-muted-foreground hover:text-foreground shrink-0"
|
|
>
|
|
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|