Commit 5b78023f authored by boojack's avatar boojack

Polish share-as-image UI and sidebar sharing actions

Made-with: Cursor
parent e51985a2
......@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent size="full" className="md:w-auto md:max-w-none" style={{ width: `${dialogWidth}px` }}>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ImageIcon className="h-4 w-4" />
<DialogContent
size="full"
className="min-h-0 overflow-hidden !gap-0 !p-0 md:w-auto md:max-w-none"
style={{ width: `${dialogWidth}px` }}
>
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<DialogHeader className="shrink-0 border-b border-border/60 px-4 py-3 sm:px-5">
<DialogTitle className="flex items-center gap-2 text-base font-medium">
<ImageIcon className="h-4 w-4 text-muted-foreground" />
{t("memo.share.image-title")}
</DialogTitle>
<DialogDescription>{t("memo.share.image-description", { width: previewWidth })}</DialogDescription>
<DialogDescription className="text-xs">{t("memo.share.image-description", { width: previewRenderWidth })}</DialogDescription>
</DialogHeader>
<div className="overflow-auto p-1 sm:p-2">
<MemoShareImagePreview ref={previewRef} width={previewWidth} />
<div className="relative flex min-h-0 flex-1 items-start justify-center overflow-auto bg-muted/20 px-4 py-3 sm:px-5 sm:py-4">
<MemoShareImagePreview ref={previewRef} width={previewRenderWidth} />
</div>
<DialogFooter>
<DialogFooter className="shrink-0 border-t border-border/60 px-4 py-3 sm:px-5">
{supportsNativeShare && (
<Button variant="outline" onClick={handleNativeShare} disabled={isRendering}>
<Button
variant="ghost"
className="text-muted-foreground hover:bg-muted/60 hover:text-foreground"
onClick={handleNativeShare}
disabled={isRendering}
>
{isRendering ? <Loader2Icon className="mr-2 h-4 w-4 animate-spin" /> : <Share2Icon className="mr-2 h-4 w-4" />}
{t("memo.share.image-share")}
</Button>
)}
<Button onClick={handleDownload} disabled={isRendering}>
<Button
variant="outline"
className="border-border/70 text-muted-foreground hover:bg-muted/60 hover:text-foreground"
onClick={handleDownload}
disabled={isRendering}
>
{isRendering ? <Loader2Icon className="mr-2 h-4 w-4 animate-spin" /> : <DownloadIcon className="mr-2 h-4 w-4" />}
{t("memo.share.image-download")}
</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
);
......
......@@ -34,18 +34,11 @@ const MemoShareImagePreview = forwardRef<HTMLDivElement, { width: number }>(({ w
}, [memo.attachments]);
return (
<div
ref={ref}
className="relative overflow-hidden rounded-[24px] border border-border/50 bg-linear-to-br from-background via-muted/15 to-background p-2.5 sm:p-3"
style={{ width }}
>
<div className="pointer-events-none absolute -top-16 right-0 h-32 w-32 rounded-full bg-sky-500/8 blur-3xl" />
<div className="pointer-events-none absolute -bottom-20 -left-10 h-36 w-36 rounded-full bg-amber-400/8 blur-3xl" />
<div className="relative overflow-hidden rounded-[20px] border border-border/60 bg-background/98 p-4 shadow-sm shadow-foreground/5 sm:p-5">
<div ref={ref} className="overflow-hidden rounded-xl border border-border/50 bg-background p-2 sm:p-2.5" style={{ width }}>
<div className="overflow-hidden rounded-lg border border-border/60 bg-background p-4 sm:p-5">
<div className="flex items-start gap-3">
<div className="flex min-w-0 items-center gap-2.5">
<UserAvatar avatarUrl={avatarUrl} className="h-9 w-9 rounded-2xl" />
<UserAvatar avatarUrl={avatarUrl} className="h-8 w-8 rounded-xl" />
<div className="min-w-0">
<div className="truncate text-[13px] font-semibold text-foreground">{displayName}</div>
{formattedDisplayTime && <div className="truncate text-xs text-muted-foreground">{formattedDisplayTime}</div>}
......@@ -65,7 +58,7 @@ const MemoShareImagePreview = forwardRef<HTMLDivElement, { width: number }>(({ w
<div
key={item.id}
className={cn(
"relative overflow-hidden rounded-[18px] border border-border/70 bg-muted/40",
"relative overflow-hidden rounded-md border border-border/70 bg-muted/30",
visualItems.length === 1 ? "aspect-[4/3]" : "aspect-square",
visualItems.length === 3 && index === 0 && "col-span-2 aspect-[2.2/1]",
)}
......
import { toBlob } from "html-to-image";
const WINDOW_HORIZONTAL_MARGIN = 32;
const PREVIEW_HORIZONTAL_PADDING_IN_DIALOG = 40;
const PREVIEW_WIDTH_BOOST_IN_DIALOG = 48;
export const MEMO_SHARE_IMAGE_CONFIG = {
dialogExtraWidth: 80,
......@@ -75,6 +77,11 @@ export const getMemoShareDialogWidth = (previewWidth: number) => {
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) => {
......
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) && (
<SidebarSection label={t("memo.share.section-label")}>
<div className="flex flex-col gap-2">
<div className="overflow-hidden rounded-md border border-border/50 bg-muted/20">
{onShareImageOpen && (
<Button variant="outline" className="w-full justify-start gap-2" onClick={onShareImageOpen}>
<ImageIcon className="w-4 h-4" />
{t("memo.share.open-image")}
<Button variant="ghost" size="sm" className={SHARE_ACTION_ROW_CLASSES} onClick={onShareImageOpen}>
<span className="flex min-w-0 flex-1 items-center gap-2">
<ImageIcon className="size-3.5 shrink-0 text-muted-foreground/90" />
<span className="truncate">{t("memo.share.open-image")}</span>
</span>
<ChevronRightIcon className="size-3.5 shrink-0 text-muted-foreground/35" />
</Button>
)}
{onShareImageOpen && canManageShares && <div className="border-t border-border/50" />}
{canManageShares && (
<Button variant="outline" className="w-full justify-start gap-2" onClick={() => setSharePanelOpen(true)}>
<Share2Icon className="w-4 h-4" />
{t("memo.share.open-panel")}
<Button variant="ghost" size="sm" className={SHARE_ACTION_ROW_CLASSES} onClick={() => setSharePanelOpen(true)}>
<span className="flex min-w-0 flex-1 items-center gap-2">
<Share2Icon className="size-3.5 shrink-0 text-muted-foreground/90" />
<span className="truncate">{t("memo.share.open-panel")}</span>
</span>
<ChevronRightIcon className="size-3.5 shrink-0 text-muted-foreground/35" />
</Button>
)}
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment