chore(web): unify metadata badge styling and fix event handling

- Remove MetadataBadge component and inline styles consistently
- Add pointer/mouse event handlers to prevent drag interference
- Fix LocationDisplay mode handling and popover interaction
- Clean up RelationList empty state logic
This commit is contained in:
Steven 2025-11-10 21:53:56 +08:00
parent 659c63165b
commit dc398cf6a7
6 changed files with 73 additions and 87 deletions

View File

@ -28,7 +28,7 @@ const AttachmentCard = ({ attachment, mode, onRemove, onClick, className, showTh
return ( return (
<div <div
className={cn( className={cn(
"group relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-background text-secondary-foreground text-xs transition-colors hover:bg-accent", "relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-background text-secondary-foreground text-xs transition-colors hover:bg-accent",
className, className,
)} )}
> >
@ -41,6 +41,12 @@ const AttachmentCard = ({ attachment, mode, onRemove, onClick, className, showTh
{onRemove && ( {onRemove && (
<button <button
className="shrink-0 rounded hover:bg-accent transition-colors p-0.5" className="shrink-0 rounded hover:bg-accent transition-colors p-0.5"
onPointerDown={(e) => {
e.stopPropagation();
}}
onMouseDown={(e) => {
e.stopPropagation();
}}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View File

@ -1,53 +1,60 @@
import { LatLng } from "leaflet"; import { LatLng } from "leaflet";
import { ExternalLinkIcon, MapPinIcon } from "lucide-react"; import { MapPinIcon, XIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { cn } from "@/lib/utils";
import { Location } from "@/types/proto/api/v1/memo_service"; import { Location } from "@/types/proto/api/v1/memo_service";
import LeafletMap from "../LeafletMap"; import LeafletMap from "../LeafletMap";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import MetadataBadge from "./MetadataBadge";
import { BaseMetadataProps } from "./types"; import { BaseMetadataProps } from "./types";
interface LocationDisplayProps extends BaseMetadataProps { interface LocationDisplayProps extends BaseMetadataProps {
location?: Location; location?: Location;
onRemove?: () => void; onRemove?: () => void;
onClick?: () => void;
} }
/** const LocationDisplay = ({ location, mode, onRemove, className }: LocationDisplayProps) => {
* Unified Location component for both editor and view modes
*
* Editor mode: Shows badge with remove button
* View mode: Shows badge with popover map on click
*/
const LocationDisplay = ({ location, mode, onRemove, onClick, className }: LocationDisplayProps) => {
const [popoverOpen, setPopoverOpen] = useState<boolean>(false); const [popoverOpen, setPopoverOpen] = useState<boolean>(false);
if (!location) { if (!location) {
return null; return null;
} }
const displayText = location.placeholder || `[${location.latitude}, ${location.longitude}]`; const displayText = location.placeholder || `Position: [${location.latitude}, ${location.longitude}]`;
// Editor mode: Simple badge with remove button
if (mode === "edit") {
return (
<div className="w-full flex flex-row flex-wrap gap-2 mt-2">
<MetadataBadge icon={<MapPinIcon className="w-3.5 h-3.5" />} onRemove={onRemove} onClick={onClick} className={className}>
{displayText}
</MetadataBadge>
</div>
);
}
// View mode: Badge with popover map
return ( return (
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}> <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<div className="w-full flex flex-row flex-wrap gap-2"> <div
<MetadataBadge icon={<MapPinIcon className="w-3.5 h-3.5" />} onClick={() => setPopoverOpen(true)} className={className}> className={cn(
<span>{displayText}</span> "w-full max-w-full flex flex-row gap-2",
<ExternalLinkIcon className="w-2.5 h-2.5 ml-1 opacity-50" /> "relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-background hover:bg-accent text-secondary-foreground text-xs transition-colors",
</MetadataBadge> mode === "view" && "cursor-pointer",
className,
)}
onClick={mode === "view" ? () => setPopoverOpen(true) : undefined}
>
<span className="shrink-0 text-muted-foreground">
<MapPinIcon className="w-3.5 h-3.5" />
</span>
<span className="text-nowrap truncate">{displayText}</span>
{onRemove && (
<button
className="shrink-0 rounded hover:bg-accent transition-colors p-0.5"
onPointerDown={(e) => {
e.stopPropagation();
}}
onMouseDown={(e) => {
e.stopPropagation();
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onRemove();
}}
>
<XIcon className="w-3 h-3 text-muted-foreground hover:text-foreground" />
</button>
)}
</div> </div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align="start"> <PopoverContent align="start">

View File

@ -1,46 +0,0 @@
import { XIcon } from "lucide-react";
import { ReactNode } from "react";
import { cn } from "@/lib/utils";
interface MetadataBadgeProps {
icon: ReactNode;
children: ReactNode;
onRemove?: () => void;
onClick?: () => void;
className?: string;
maxWidth?: string;
}
/**
* Shared badge component for metadata display (Location, Tags, etc.)
* Provides consistent styling across editor and view modes
*/
const MetadataBadge = ({ icon, children, onRemove, onClick, className, maxWidth = "max-w-[160px]" }: MetadataBadgeProps) => {
return (
<div
className={cn(
"group relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-background text-secondary-foreground text-xs transition-colors",
onClick && "cursor-pointer hover:bg-accent",
className,
)}
onClick={onClick}
>
<span className="shrink-0 text-muted-foreground">{icon}</span>
<span className={cn("truncate", maxWidth)}>{children}</span>
{onRemove && (
<button
className="shrink-0 rounded hover:bg-accent transition-colors p-0.5"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onRemove();
}}
>
<XIcon className="w-3 h-3 text-muted-foreground hover:text-foreground" />
</button>
)}
</div>
);
};
export default MetadataBadge;

View File

@ -27,14 +27,30 @@ const RelationCard = ({ memo, mode, onRemove, parentPage, className }: RelationC
return ( return (
<div <div
className={cn( className={cn(
"group relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-background text-secondary-foreground text-xs transition-colors hover:bg-accent cursor-pointer", "relative inline-flex items-center gap-1.5 px-2 h-7 rounded-md border border-border bg-background text-secondary-foreground text-xs transition-colors hover:bg-accent",
className, className,
)} )}
onClick={onRemove}
> >
<LinkIcon className="w-3.5 h-3.5 shrink-0 text-muted-foreground" /> <LinkIcon className="w-3.5 h-3.5 shrink-0 text-muted-foreground" />
<span className="truncate max-w-[160px]">{memo.snippet}</span> <span className="truncate max-w-[160px]">{memo.snippet}</span>
<XIcon className="w-3 h-3 shrink-0 text-muted-foreground" /> {onRemove && (
<button
className="shrink-0 rounded hover:bg-accent transition-colors p-0.5"
onPointerDown={(e) => {
e.stopPropagation();
}}
onMouseDown={(e) => {
e.stopPropagation();
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onRemove();
}}
>
<XIcon className="w-3 h-3 text-muted-foreground hover:text-foreground" />
</button>
)}
</div> </div>
); );
} }
@ -43,7 +59,7 @@ const RelationCard = ({ memo, mode, onRemove, parentPage, className }: RelationC
return ( return (
<Link <Link
className={cn( className={cn(
"w-full flex flex-row justify-start items-center text-sm leading-5 text-muted-foreground hover:text-foreground hover:bg-accent rounded px-2 py-1 transition-colors", "w-full flex flex-row justify-start items-center text-sm leading-5 text-muted-foreground hover:text-foreground hover:bg-accent rounded px-1 py-1 transition-colors",
className, className,
)} )}
to={`/${memo.name}`} to={`/${memo.name}`}
@ -52,7 +68,7 @@ const RelationCard = ({ memo, mode, onRemove, parentPage, className }: RelationC
from: parentPage, from: parentPage,
}} }}
> >
<span className="text-xs opacity-60 leading-4 border border-border font-mono px-1 rounded-full mr-1">{memoId.slice(0, 6)}</span> <span className="text-[10px] opacity-60 leading-4 border border-border font-mono px-1 rounded-full mr-1">{memoId.slice(0, 6)}</span>
<span className="truncate">{memo.snippet}</span> <span className="truncate">{memo.snippet}</span>
</Link> </Link>
); );

View File

@ -52,13 +52,17 @@ const RelationList = observer(({ relations, currentMemoName, mode, onRelationsCh
// Fetch full memo details for editor mode // Fetch full memo details for editor mode
useEffect(() => { useEffect(() => {
if (mode === "edit" && referencingRelations.length > 0) { if (mode === "edit") {
(async () => { (async () => {
const requests = referencingRelations.map(async (relation) => { if (referencingRelations.length > 0) {
return await memoStore.getOrFetchMemoByName(relation.relatedMemo!.name, { skipStore: true }); const requests = referencingRelations.map(async (relation) => {
}); return await memoStore.getOrFetchMemoByName(relation.relatedMemo!.name, { skipStore: true });
const list = await Promise.all(requests); });
setReferencingMemos(list); const list = await Promise.all(requests);
setReferencingMemos(list);
} else {
setReferencingMemos([]);
}
})(); })();
} }
}, [mode, relations]); }, [mode, relations]);

View File

@ -8,7 +8,6 @@ export { default as AttachmentList } from "./AttachmentList";
export { default as RelationList } from "./RelationList"; export { default as RelationList } from "./RelationList";
// Base components (can be used for other metadata types) // Base components (can be used for other metadata types)
export { default as MetadataBadge } from "./MetadataBadge";
export { default as MetadataCard } from "./MetadataCard"; export { default as MetadataCard } from "./MetadataCard";
export { default as AttachmentCard } from "./AttachmentCard"; export { default as AttachmentCard } from "./AttachmentCard";
export { default as RelationCard } from "./RelationCard"; export { default as RelationCard } from "./RelationCard";