memos/web/src/components/MemoEditor/Toolbar/VisibilitySelector.tsx
boojack daa71d0456 feat(editor): recognize and style @mentions in the rich editor
Mirror the #tag mark so the TipTap editor styles @username while typing
and on load, matching the read-only view. A shared mention grammar
(utils/mention-grammar.ts) feeds both the editor tokenizer and the
read-only remark renderer so they can't drift.

Styling only -- no autocomplete dropdown (ListUsers is admin-only and
there is no user-search RPC). Bare emails still autolink to mailto: and
are never treated as mentions: the tokenizer's start() skips email-glued
@s so marked's GFM autolinker keeps seeing the whole address.
2026-06-26 09:28:41 +08:00

42 lines
1.9 KiB
TypeScript

import { CheckIcon, ChevronDownIcon } from "lucide-react";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import VisibilityIcon from "@/components/VisibilityIcon";
import { Visibility } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
import type { VisibilitySelectorProps } from "../types";
const VisibilitySelector = (props: VisibilitySelectorProps) => {
const { value, onChange } = props;
const t = useTranslate();
const visibilityOptions = [
{ value: Visibility.PRIVATE, label: t("memo.visibility.private") },
{ value: Visibility.PROTECTED, label: t("memo.visibility.protected") },
{ value: Visibility.PUBLIC, label: t("memo.visibility.public") },
] as const;
const currentLabel = visibilityOptions.find((option) => option.value === value)?.label || "";
return (
<DropdownMenu onOpenChange={props.onOpenChange}>
<DropdownMenuTrigger asChild>
<button className="inline-flex items-center h-8 px-2 rounded-md text-sm text-muted-foreground hover:bg-accent transition-colors">
<VisibilityIcon visibility={value} className="opacity-60 mr-1.5" />
<span>{currentLabel}</span>
<ChevronDownIcon className="ml-0.5 w-4 h-4 opacity-60" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{visibilityOptions.map((option) => (
<DropdownMenuItem key={option.value} className="cursor-pointer gap-2" onClick={() => onChange(option.value)}>
<VisibilityIcon visibility={option.value} />
<span className="flex-1">{option.label}</span>
{value === option.value && <CheckIcon className="w-4 h-4 text-primary" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};
export default VisibilitySelector;