multica/apps/web/components/common/reaction-bar.tsx
Naiyuan Qing 9a37af4ca1 feat(ui): editor UX improvements — lowlight, upload, emoji, comment editing
- New QuickEmojiPicker: shared SmilePlus + 8 quick emojis + full picker
- New FileUploadButton: reusable Paperclip upload trigger
- New CodeBlockView: React NodeView with language label + copy button
- CodeBlockLowlight: syntax highlighting in editor (replaces plain codeBlock)
- ReactionBar: brand-tinted pill styles, hideAddButton prop
- Comment header: emoji picker + three-dot menu in top-right
- Comment edit: inline editing with brand border, blur-to-save, Escape-to-cancel
- RichTextEditor: add onBlur prop, markdown paste extension
- Create issue: upload button in footer
- Issue detail: upload button next to reaction bar
- Comment/reply: use FileUploadButton, loading spinners, no optimistic updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 18:37:53 +08:00

82 lines
2.3 KiB
TypeScript

"use client";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { QuickEmojiPicker } from "@/components/common/quick-emoji-picker";
import { useActorName } from "@/features/workspace";
interface ReactionItem {
id: string;
actor_type: string;
actor_id: string;
emoji: string;
}
interface GroupedReaction {
emoji: string;
count: number;
reacted: boolean;
actors: { type: string; id: string }[];
}
function groupReactions(reactions: ReactionItem[], currentUserId?: string): GroupedReaction[] {
const map = new Map<string, GroupedReaction>();
for (const r of reactions) {
let group = map.get(r.emoji);
if (!group) {
group = { emoji: r.emoji, count: 0, reacted: false, actors: [] };
map.set(r.emoji, group);
}
group.count++;
group.actors.push({ type: r.actor_type, id: r.actor_id });
if (r.actor_type === "member" && r.actor_id === currentUserId) {
group.reacted = true;
}
}
return Array.from(map.values());
}
export function ReactionBar({
reactions,
currentUserId,
onToggle,
className,
hideAddButton,
}: {
reactions: ReactionItem[];
currentUserId?: string;
onToggle: (emoji: string) => void;
className?: string;
hideAddButton?: boolean;
}) {
const grouped = groupReactions(reactions, currentUserId);
const { getActorName } = useActorName();
return (
<div className={`flex flex-wrap items-center gap-1.5 ${className ?? ""}`}>
{grouped.map((g) => (
<Tooltip key={g.emoji}>
<TooltipTrigger
render={
<button
type="button"
onClick={() => onToggle(g.emoji)}
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs transition-colors hover:bg-brand/15 ${
g.reacted
? "border-brand/30 bg-brand/8 text-brand"
: "border-brand/10 bg-brand/4 text-muted-foreground"
}`}
>
<span>{g.emoji}</span>
<span>{g.count}</span>
</button>
}
/>
<TooltipContent side="top">
{g.actors.map((a) => getActorName(a.type, a.id)).join(", ")}
</TooltipContent>
</Tooltip>
))}
{!hideAddButton && <QuickEmojiPicker onSelect={onToggle} />}
</div>
);
}