memos/web/src/components/MemoEditor/Toolbar/VisibilitySelector.tsx
boojack d4b5a1695b feat(web): redesign memo detail sidebar as property rail
Rework MemoDetailSidebar into a Linear-style rail: a quiet icon action
cluster (pin, copy link, share image, share links) followed by
label/value property rows for visibility, created/edited times, author,
and location, then hairline-separated sections for tags, attachments,
relations, and the outline. Attachments and reference relations were
previously not surfaced in the sidebar at all.

- Visibility is editable in place, reusing VisibilitySelector with a
  new compact size variant that matches the 13px row grammar
- Pin toggle and visibility changes go through useUpdateMemo
- MemoOutline gains active-section tracking (scroll-spy), depth
  indentation, and tick-rail styling; drops the animated underline
  and native title tooltips
- Widen the detail rail from w-56 to w-60
2026-07-16 23:59:31 +08:00

54 lines
2.5 KiB
TypeScript

import { CheckIcon, ChevronDownIcon } from "lucide-react";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import VisibilityIcon from "@/components/VisibilityIcon";
import { cn } from "@/lib/utils";
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 compact = props.size === "compact";
const t = useTranslate();
const visibilityOptions = [
{ value: Visibility.PRIVATE, label: t("memo.visibility.private"), description: t("memo.visibility.private-description") },
{ value: Visibility.PROTECTED, label: t("memo.visibility.protected"), description: t("memo.visibility.protected-description") },
{ value: Visibility.PUBLIC, label: t("memo.visibility.public"), description: t("memo.visibility.public-description") },
] as const;
const currentLabel = visibilityOptions.find((option) => option.value === value)?.label || "";
return (
<DropdownMenu onOpenChange={props.onOpenChange}>
<DropdownMenuTrigger
render={
<button
className={cn(
"inline-flex items-center rounded-md hover:bg-accent transition-colors",
compact ? "px-1.5 py-[3px] text-[13px] leading-5 text-foreground/85" : "h-8 px-2 text-sm text-muted-foreground",
)}
/>
}
>
<VisibilityIcon visibility={value} className={cn("opacity-60 mr-1.5", compact && "w-[13px]")} />
<span className="truncate">{currentLabel}</span>
<ChevronDownIcon className={cn("ml-0.5 opacity-60", compact ? "size-3.5 text-muted-foreground/70" : "w-4 h-4")} />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{visibilityOptions.map((option) => (
<DropdownMenuItem key={option.value} onClick={() => onChange(option.value)}>
<VisibilityIcon visibility={option.value} />
<div className="flex flex-col">
<span>{option.label}</span>
<span className="text-xs text-muted-foreground">{option.description}</span>
</div>
{value === option.value && <CheckIcon className="ml-auto w-4 h-4 text-primary" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};
export default VisibilitySelector;