Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
5b78023f
Commit
5b78023f
authored
Apr 07, 2026
by
boojack
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Polish share-as-image UI and sidebar sharing actions
Made-with: Cursor
parent
e51985a2
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
75 additions
and
42 deletions
+75
-42
MemoShareImageDialog.tsx
web/src/components/MemoActionMenu/MemoShareImageDialog.tsx
+46
-23
MemoShareImagePreview.tsx
web/src/components/MemoActionMenu/MemoShareImagePreview.tsx
+4
-11
memoShareImage.ts
web/src/components/MemoActionMenu/memoShareImage.ts
+7
-0
MemoDetailSidebar.tsx
web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx
+18
-8
No files found.
web/src/components/MemoActionMenu/MemoShareImageDialog.tsx
View file @
5b78023f
...
@@ -6,7 +6,13 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
...
@@ -6,7 +6,13 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
useMemoViewContext
}
from
"../MemoView/MemoViewContext"
;
import
{
useMemoViewContext
}
from
"../MemoView/MemoViewContext"
;
import
MemoShareImagePreview
from
"./MemoShareImagePreview"
;
import
MemoShareImagePreview
from
"./MemoShareImagePreview"
;
import
{
buildMemoShareImageFileName
,
createMemoShareImageBlob
,
getMemoShareDialogWidth
,
getMemoSharePreviewWidth
}
from
"./memoShareImage"
;
import
{
buildMemoShareImageFileName
,
createMemoShareImageBlob
,
getMemoShareDialogWidth
,
getMemoSharePreviewWidth
,
getMemoShareRenderWidth
,
}
from
"./memoShareImage"
;
interface
MemoShareImageDialogProps
{
interface
MemoShareImageDialogProps
{
open
:
boolean
;
open
:
boolean
;
...
@@ -21,6 +27,7 @@ const MemoShareImageDialog = ({ open, onOpenChange }: MemoShareImageDialogProps)
...
@@ -21,6 +27,7 @@ const MemoShareImageDialog = ({ open, onOpenChange }: MemoShareImageDialogProps)
const
previewWidth
=
useMemo
(()
=>
getMemoSharePreviewWidth
(
cardWidth
),
[
cardWidth
]);
const
previewWidth
=
useMemo
(()
=>
getMemoSharePreviewWidth
(
cardWidth
),
[
cardWidth
]);
const
dialogWidth
=
useMemo
(()
=>
getMemoShareDialogWidth
(
previewWidth
),
[
previewWidth
]);
const
dialogWidth
=
useMemo
(()
=>
getMemoShareDialogWidth
(
previewWidth
),
[
previewWidth
]);
const
previewRenderWidth
=
useMemo
(()
=>
getMemoShareRenderWidth
(
previewWidth
,
dialogWidth
),
[
dialogWidth
,
previewWidth
]);
const
createShareBlob
=
useCallback
(
async
()
=>
{
const
createShareBlob
=
useCallback
(
async
()
=>
{
const
preview
=
previewRef
.
current
;
const
preview
=
previewRef
.
current
;
...
@@ -81,31 +88,47 @@ const MemoShareImageDialog = ({ open, onOpenChange }: MemoShareImageDialogProps)
...
@@ -81,31 +88,47 @@ const MemoShareImageDialog = ({ open, onOpenChange }: MemoShareImageDialogProps)
return
(
return
(
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
Dialog
open=
{
open
}
onOpenChange=
{
onOpenChange
}
>
<
DialogContent
size=
"full"
className=
"md:w-auto md:max-w-none"
style=
{
{
width
:
`${dialogWidth}px`
}
}
>
<
DialogContent
<
DialogHeader
>
size=
"full"
<
DialogTitle
className=
"flex items-center gap-2"
>
className=
"min-h-0 overflow-hidden !gap-0 !p-0 md:w-auto md:max-w-none"
<
ImageIcon
className=
"h-4 w-4"
/>
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"
)
}
{
t
(
"memo.share.image-title"
)
}
</
DialogTitle
>
</
DialogTitle
>
<
DialogDescription
>
{
t
(
"memo.share.image-description"
,
{
width
:
preview
Width
})
}
</
DialogDescription
>
<
DialogDescription
className=
"text-xs"
>
{
t
(
"memo.share.image-description"
,
{
width
:
previewRender
Width
})
}
</
DialogDescription
>
</
DialogHeader
>
</
DialogHeader
>
<
div
className=
"overflow-auto p-1 sm:p-2
"
>
<
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=
{
preview
Width
}
/>
<
MemoShareImagePreview
ref=
{
previewRef
}
width=
{
previewRender
Width
}
/>
</
div
>
</
div
>
<
DialogFooter
>
<
DialogFooter
className=
"shrink-0 border-t border-border/60 px-4 py-3 sm:px-5"
>
{
supportsNativeShare
&&
(
{
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"
/>
}
{
isRendering
?
<
Loader2Icon
className=
"mr-2 h-4 w-4 animate-spin"
/>
:
<
Share2Icon
className=
"mr-2 h-4 w-4"
/>
}
{
t
(
"memo.share.image-share"
)
}
{
t
(
"memo.share.image-share"
)
}
</
Button
>
</
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"
/>
}
{
isRendering
?
<
Loader2Icon
className=
"mr-2 h-4 w-4 animate-spin"
/>
:
<
DownloadIcon
className=
"mr-2 h-4 w-4"
/>
}
{
t
(
"memo.share.image-download"
)
}
{
t
(
"memo.share.image-download"
)
}
</
Button
>
</
Button
>
</
DialogFooter
>
</
DialogFooter
>
</
div
>
</
DialogContent
>
</
DialogContent
>
</
Dialog
>
</
Dialog
>
);
);
...
...
web/src/components/MemoActionMenu/MemoShareImagePreview.tsx
View file @
5b78023f
...
@@ -34,18 +34,11 @@ const MemoShareImagePreview = forwardRef<HTMLDivElement, { width: number }>(({ w
...
@@ -34,18 +34,11 @@ const MemoShareImagePreview = forwardRef<HTMLDivElement, { width: number }>(({ w
},
[
memo
.
attachments
]);
},
[
memo
.
attachments
]);
return
(
return
(
<
div
<
div
ref=
{
ref
}
className=
"overflow-hidden rounded-xl border border-border/50 bg-background p-2 sm:p-2.5"
style=
{
{
width
}
}
>
ref=
{
ref
}
<
div
className=
"overflow-hidden rounded-lg border border-border/60 bg-background p-4 sm:p-5"
>
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
className=
"flex items-start gap-3"
>
<
div
className=
"flex items-start gap-3"
>
<
div
className=
"flex min-w-0 items-center gap-2.5"
>
<
div
className=
"flex min-w-0 items-center gap-2.5"
>
<
UserAvatar
avatarUrl=
{
avatarUrl
}
className=
"h-
9 w-9 rounded-2
xl"
/>
<
UserAvatar
avatarUrl=
{
avatarUrl
}
className=
"h-
8 w-8 rounded-
xl"
/>
<
div
className=
"min-w-0"
>
<
div
className=
"min-w-0"
>
<
div
className=
"truncate text-[13px] font-semibold text-foreground"
>
{
displayName
}
</
div
>
<
div
className=
"truncate text-[13px] font-semibold text-foreground"
>
{
displayName
}
</
div
>
{
formattedDisplayTime
&&
<
div
className=
"truncate text-xs text-muted-foreground"
>
{
formattedDisplayTime
}
</
div
>
}
{
formattedDisplayTime
&&
<
div
className=
"truncate text-xs text-muted-foreground"
>
{
formattedDisplayTime
}
</
div
>
}
...
@@ -65,7 +58,7 @@ const MemoShareImagePreview = forwardRef<HTMLDivElement, { width: number }>(({ w
...
@@ -65,7 +58,7 @@ const MemoShareImagePreview = forwardRef<HTMLDivElement, { width: number }>(({ w
<
div
<
div
key=
{
item
.
id
}
key=
{
item
.
id
}
className=
{
cn
(
className=
{
cn
(
"relative overflow-hidden rounded-
[18px] border border-border/70 bg-muted/4
0"
,
"relative overflow-hidden rounded-
md border border-border/70 bg-muted/3
0"
,
visualItems
.
length
===
1
?
"aspect-[4/3]"
:
"aspect-square"
,
visualItems
.
length
===
1
?
"aspect-[4/3]"
:
"aspect-square"
,
visualItems
.
length
===
3
&&
index
===
0
&&
"col-span-2 aspect-[2.2/1]"
,
visualItems
.
length
===
3
&&
index
===
0
&&
"col-span-2 aspect-[2.2/1]"
,
)
}
)
}
...
...
web/src/components/MemoActionMenu/memoShareImage.ts
View file @
5b78023f
import
{
toBlob
}
from
"html-to-image"
;
import
{
toBlob
}
from
"html-to-image"
;
const
WINDOW_HORIZONTAL_MARGIN
=
32
;
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
=
{
export
const
MEMO_SHARE_IMAGE_CONFIG
=
{
dialogExtraWidth
:
80
,
dialogExtraWidth
:
80
,
...
@@ -75,6 +77,11 @@ export const getMemoShareDialogWidth = (previewWidth: number) => {
...
@@ -75,6 +77,11 @@ export const getMemoShareDialogWidth = (previewWidth: number) => {
return
Math
.
min
(
previewWidth
+
MEMO_SHARE_IMAGE_CONFIG
.
dialogExtraWidth
,
viewportWidth
);
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
getMemoSharePreviewAvatarUrl
=
(
avatarUrl
?:
string
)
=>
(
isExportableImageUrl
(
avatarUrl
)
?
avatarUrl
:
undefined
);
export
const
createMemoShareImageBlob
=
async
(
node
:
HTMLElement
)
=>
{
export
const
createMemoShareImageBlob
=
async
(
node
:
HTMLElement
)
=>
{
...
...
web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx
View file @
5b78023f
import
{
create
}
from
"@bufbuild/protobuf"
;
import
{
create
}
from
"@bufbuild/protobuf"
;
import
{
timestampDate
}
from
"@bufbuild/protobuf/wkt"
;
import
{
timestampDate
}
from
"@bufbuild/protobuf/wkt"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
CheckCircleIcon
,
Code2Icon
,
HashIcon
,
ImageIcon
,
LinkIcon
,
type
LucideIcon
,
Share2Icon
}
from
"lucide-react"
;
import
{
CheckCircleIcon
,
C
hevronRightIcon
,
C
ode2Icon
,
HashIcon
,
ImageIcon
,
LinkIcon
,
type
LucideIcon
,
Share2Icon
}
from
"lucide-react"
;
import
{
useMemo
,
useState
}
from
"react"
;
import
{
useMemo
,
useState
}
from
"react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
...
@@ -40,6 +40,9 @@ const PROPERTY_BADGE_CLASSES =
...
@@ -40,6 +40,9 @@ const PROPERTY_BADGE_CLASSES =
const
TAG_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"
;
"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
MemoDetailSidebar
=
({
memo
,
className
,
onShareImageOpen
}:
Props
)
=>
{
const
t
=
useTranslate
();
const
t
=
useTranslate
();
const
currentUser
=
useCurrentUser
();
const
currentUser
=
useCurrentUser
();
...
@@ -67,17 +70,24 @@ const MemoDetailSidebar = ({ memo, className, onShareImageOpen }: Props) => {
...
@@ -67,17 +70,24 @@ const MemoDetailSidebar = ({ memo, className, onShareImageOpen }: Props) => {
{
(
canManageShares
||
onShareImageOpen
)
&&
(
{
(
canManageShares
||
onShareImageOpen
)
&&
(
<
SidebarSection
label=
{
t
(
"memo.share.section-label"
)
}
>
<
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
&&
(
{
onShareImageOpen
&&
(
<
Button
variant=
"outline"
className=
"w-full justify-start gap-2"
onClick=
{
onShareImageOpen
}
>
<
Button
variant=
"ghost"
size=
"sm"
className=
{
SHARE_ACTION_ROW_CLASSES
}
onClick=
{
onShareImageOpen
}
>
<
ImageIcon
className=
"w-4 h-4"
/>
<
span
className=
"flex min-w-0 flex-1 items-center gap-2"
>
{
t
(
"memo.share.open-image"
)
}
<
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
>
</
Button
>
)
}
)
}
{
onShareImageOpen
&&
canManageShares
&&
<
div
className=
"border-t border-border/50"
/>
}
{
canManageShares
&&
(
{
canManageShares
&&
(
<
Button
variant=
"outline"
className=
"w-full justify-start gap-2"
onClick=
{
()
=>
setSharePanelOpen
(
true
)
}
>
<
Button
variant=
"ghost"
size=
"sm"
className=
{
SHARE_ACTION_ROW_CLASSES
}
onClick=
{
()
=>
setSharePanelOpen
(
true
)
}
>
<
Share2Icon
className=
"w-4 h-4"
/>
<
span
className=
"flex min-w-0 flex-1 items-center gap-2"
>
{
t
(
"memo.share.open-panel"
)
}
<
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
>
</
Button
>
)
}
)
}
</
div
>
</
div
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment