From 5b78023fd5eec34ab6bb348bac8abfe54ff4a04f Mon Sep 17 00:00:00 2001 From: boojack Date: Tue, 7 Apr 2026 21:07:40 +0800 Subject: [PATCH] Polish share-as-image UI and sidebar sharing actions Made-with: Cursor --- .../MemoActionMenu/MemoShareImageDialog.tsx | 69 ++++++++++++------- .../MemoActionMenu/MemoShareImagePreview.tsx | 15 ++-- .../MemoActionMenu/memoShareImage.ts | 7 ++ .../MemoDetailSidebar/MemoDetailSidebar.tsx | 26 ++++--- 4 files changed, 75 insertions(+), 42 deletions(-) diff --git a/web/src/components/MemoActionMenu/MemoShareImageDialog.tsx b/web/src/components/MemoActionMenu/MemoShareImageDialog.tsx index 3427a41de..6308e50f1 100644 --- a/web/src/components/MemoActionMenu/MemoShareImageDialog.tsx +++ b/web/src/components/MemoActionMenu/MemoShareImageDialog.tsx @@ -6,7 +6,13 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { useTranslate } from "@/utils/i18n"; import { useMemoViewContext } from "../MemoView/MemoViewContext"; import MemoShareImagePreview from "./MemoShareImagePreview"; -import { buildMemoShareImageFileName, createMemoShareImageBlob, getMemoShareDialogWidth, getMemoSharePreviewWidth } from "./memoShareImage"; +import { + buildMemoShareImageFileName, + createMemoShareImageBlob, + getMemoShareDialogWidth, + getMemoSharePreviewWidth, + getMemoShareRenderWidth, +} from "./memoShareImage"; interface MemoShareImageDialogProps { open: boolean; @@ -21,6 +27,7 @@ const MemoShareImageDialog = ({ open, onOpenChange }: MemoShareImageDialogProps) const previewWidth = useMemo(() => getMemoSharePreviewWidth(cardWidth), [cardWidth]); const dialogWidth = useMemo(() => getMemoShareDialogWidth(previewWidth), [previewWidth]); + const previewRenderWidth = useMemo(() => getMemoShareRenderWidth(previewWidth, dialogWidth), [dialogWidth, previewWidth]); const createShareBlob = useCallback(async () => { const preview = previewRef.current; @@ -81,31 +88,47 @@ const MemoShareImageDialog = ({ open, onOpenChange }: MemoShareImageDialogProps) return ( - - - - - {t("memo.share.image-title")} - - {t("memo.share.image-description", { width: previewWidth })} - + +
+ + + + {t("memo.share.image-title")} + + {t("memo.share.image-description", { width: previewRenderWidth })} + -
- -
+
+ +
- - {supportsNativeShare && ( - + )} + - )} - - + +
); diff --git a/web/src/components/MemoActionMenu/MemoShareImagePreview.tsx b/web/src/components/MemoActionMenu/MemoShareImagePreview.tsx index e86617cc3..4a6144dd0 100644 --- a/web/src/components/MemoActionMenu/MemoShareImagePreview.tsx +++ b/web/src/components/MemoActionMenu/MemoShareImagePreview.tsx @@ -34,18 +34,11 @@ const MemoShareImagePreview = forwardRef(({ w }, [memo.attachments]); return ( -
-
-
- -
+
+
- +
{displayName}
{formattedDisplayTime &&
{formattedDisplayTime}
} @@ -65,7 +58,7 @@ const MemoShareImagePreview = forwardRef(({ w
{ return Math.min(previewWidth + MEMO_SHARE_IMAGE_CONFIG.dialogExtraWidth, viewportWidth); }; +export const getMemoShareRenderWidth = (previewWidth: number, dialogWidth: number) => { + const maxRenderWidth = Math.max(MEMO_SHARE_IMAGE_CONFIG.minWidth, dialogWidth - PREVIEW_HORIZONTAL_PADDING_IN_DIALOG); + return clamp(previewWidth + PREVIEW_WIDTH_BOOST_IN_DIALOG, MEMO_SHARE_IMAGE_CONFIG.minWidth, maxRenderWidth); +}; + export const getMemoSharePreviewAvatarUrl = (avatarUrl?: string) => (isExportableImageUrl(avatarUrl) ? avatarUrl : undefined); export const createMemoShareImageBlob = async (node: HTMLElement) => { diff --git a/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx b/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx index c944bca6b..585ba6d89 100644 --- a/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx +++ b/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx @@ -1,7 +1,7 @@ import { create } from "@bufbuild/protobuf"; import { timestampDate } from "@bufbuild/protobuf/wkt"; import { isEqual } from "lodash-es"; -import { CheckCircleIcon, Code2Icon, HashIcon, ImageIcon, LinkIcon, type LucideIcon, Share2Icon } from "lucide-react"; +import { CheckCircleIcon, ChevronRightIcon, Code2Icon, HashIcon, ImageIcon, LinkIcon, type LucideIcon, Share2Icon } from "lucide-react"; import { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import useCurrentUser from "@/hooks/useCurrentUser"; @@ -40,6 +40,9 @@ const PROPERTY_BADGE_CLASSES = const TAG_BADGE_CLASSES = "inline-flex items-center gap-1 px-1 rounded-md border border-border/60 bg-muted/60 text-sm text-muted-foreground hover:bg-muted hover:text-foreground/80 transition-colors cursor-pointer"; +const SHARE_ACTION_ROW_CLASSES = + "h-auto min-h-0 w-full justify-between rounded-none px-2 py-1.5 text-xs font-normal leading-tight text-muted-foreground transition-colors hover:bg-muted/40 hover:text-muted-foreground focus-visible:ring-offset-0 gap-1.5"; + const MemoDetailSidebar = ({ memo, className, onShareImageOpen }: Props) => { const t = useTranslate(); const currentUser = useCurrentUser(); @@ -67,17 +70,24 @@ const MemoDetailSidebar = ({ memo, className, onShareImageOpen }: Props) => { {(canManageShares || onShareImageOpen) && ( -
+
{onShareImageOpen && ( - )} + {onShareImageOpen && canManageShares &&
} {canManageShares && ( - )}