UMBRA/frontend/src/components/shared/CopyableField.tsx
Kyle Pope 1231c4b36d Polish entity pages: groups, favourite toggle, copy icon, initials, notes
- 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>
2026-02-25 00:21:23 +08:00

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>
);
}