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
493832ae
Commit
493832ae
authored
Jul 01, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor: unify components
parent
50a41a39
Changes
111
Hide whitespace changes
Inline
Side-by-side
Showing
111 changed files
with
2760 additions
and
1644 deletions
+2760
-1644
components.json
web/components.json
+21
-0
package.json
web/package.json
+12
-2
pnpm-lock.yaml
web/pnpm-lock.yaml
+981
-881
App.tsx
web/src/App.tsx
+2
-3
ActivityCalendar.tsx
web/src/components/ActivityCalendar/ActivityCalendar.tsx
+12
-5
AppearanceSelect.tsx
web/src/components/AppearanceSelect.tsx
+15
-16
AttachmentIcon.tsx
web/src/components/AttachmentIcon.tsx
+1
-1
AuthFooter.tsx
web/src/components/AuthFooter.tsx
+1
-1
BrandBanner.tsx
web/src/components/BrandBanner.tsx
+1
-1
ChangeMemberPasswordDialog.tsx
web/src/components/ChangeMemberPasswordDialog.tsx
+4
-3
CreateAccessTokenDialog.tsx
web/src/components/CreateAccessTokenDialog.tsx
+14
-9
CreateIdentityProviderDialog.tsx
web/src/components/CreateIdentityProviderDialog.tsx
+66
-78
CreateShortcutDialog.tsx
web/src/components/CreateShortcutDialog.tsx
+6
-4
CreateUserDialog.tsx
web/src/components/CreateUserDialog.tsx
+17
-9
CreateWebhookDialog.tsx
web/src/components/CreateWebhookDialog.tsx
+4
-3
DateTimeInput.tsx
web/src/components/DateTimeInput.tsx
+1
-1
BaseDialog.tsx
web/src/components/Dialog/BaseDialog.tsx
+4
-8
HomeSidebar.tsx
web/src/components/HomeSidebar/HomeSidebar.tsx
+1
-1
HomeSidebarDrawer.tsx
web/src/components/HomeSidebar/HomeSidebarDrawer.tsx
+12
-20
ShortcutsSection.tsx
web/src/components/HomeSidebar/ShortcutsSection.tsx
+13
-6
TagsSection.tsx
web/src/components/HomeSidebar/TagsSection.tsx
+4
-4
MemoCommentMessage.tsx
web/src/components/Inbox/MemoCommentMessage.tsx
+25
-11
LearnMore.tsx
web/src/components/LearnMore.tsx
+13
-6
LocaleSelect.tsx
web/src/components/LocaleSelect.tsx
+28
-25
MasonryView.tsx
web/src/components/MasonryView/MasonryView.tsx
+1
-1
MemoActionMenu.tsx
web/src/components/MemoActionMenu.tsx
+2
-2
MemoAttachmentListView.tsx
web/src/components/MemoAttachmentListView.tsx
+1
-1
CodeBlock.tsx
web/src/components/MemoContent/CodeBlock.tsx
+1
-1
EmbeddedAttachment.tsx
...onents/MemoContent/EmbeddedContent/EmbeddedAttachment.tsx
+1
-1
EmbeddedMemo.tsx
...c/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx
+1
-1
HorizontalRule.tsx
web/src/components/MemoContent/HorizontalRule.tsx
+2
-2
Link.tsx
web/src/components/MemoContent/Link.tsx
+32
-27
List.tsx
web/src/components/MemoContent/List.tsx
+1
-1
Math.tsx
web/src/components/MemoContent/Math.tsx
+1
-1
MermaidBlock.tsx
web/src/components/MemoContent/MermaidBlock.tsx
+21
-3
Spoiler.tsx
web/src/components/MemoContent/Spoiler.tsx
+1
-1
Tag.tsx
web/src/components/MemoContent/Tag.tsx
+1
-1
TaskListItem.tsx
web/src/components/MemoContent/TaskListItem.tsx
+8
-3
index.tsx
web/src/components/MemoContent/index.tsx
+1
-1
MemoDetailSidebar.tsx
web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx
+1
-1
MemoDetailSidebarDrawer.tsx
.../components/MemoDetailSidebar/MemoDetailSidebarDrawer.tsx
+12
-19
MemoDisplaySettingMenu.tsx
web/src/components/MemoDisplaySettingMenu.tsx
+21
-11
AddMemoRelationPopover.tsx
...onents/MemoEditor/ActionButton/AddMemoRelationPopover.tsx
+61
-42
LocationSelector.tsx
...c/components/MemoEditor/ActionButton/LocationSelector.tsx
+31
-25
MarkdownMenu.tsx
web/src/components/MemoEditor/ActionButton/MarkdownMenu.tsx
+12
-9
TagSelector.tsx
web/src/components/MemoEditor/ActionButton/TagSelector.tsx
+4
-4
UploadAttachmentButton.tsx
...onents/MemoEditor/ActionButton/UploadAttachmentButton.tsx
+2
-2
VisibilitySelector.tsx
...components/MemoEditor/ActionButton/VisibilitySelector.tsx
+2
-2
TagSuggestions.tsx
web/src/components/MemoEditor/Editor/TagSuggestions.tsx
+1
-1
index.tsx
web/src/components/MemoEditor/Editor/index.tsx
+1
-1
index.tsx
web/src/components/MemoEditor/index.tsx
+4
-4
MemoLocationView.tsx
web/src/components/MemoLocationView.tsx
+1
-1
MemoRelationForceGraph.tsx
...ponents/MemoRelationForceGraph/MemoRelationForceGraph.tsx
+21
-3
MemoRelationListView.tsx
web/src/components/MemoRelationListView.tsx
+1
-1
MemoView.tsx
web/src/components/MemoView.tsx
+21
-11
MobileHeader.tsx
web/src/components/MobileHeader.tsx
+1
-1
Navigation.tsx
web/src/components/Navigation.tsx
+12
-5
NavigationDrawer.tsx
web/src/components/NavigationDrawer.tsx
+15
-23
PagedMemoList.tsx
web/src/components/PagedMemoList/PagedMemoList.tsx
+2
-2
PasswordSignInForm.tsx
web/src/components/PasswordSignInForm.tsx
+5
-13
PreviewImageDialog.tsx
web/src/components/PreviewImageDialog.tsx
+1
-1
ReactionSelector.tsx
web/src/components/ReactionSelector.tsx
+2
-2
ReactionView.tsx
web/src/components/ReactionView.tsx
+23
-16
RenameTagDialog.tsx
web/src/components/RenameTagDialog.tsx
+9
-9
SearchBar.tsx
web/src/components/SearchBar.tsx
+1
-1
AccessTokenSection.tsx
web/src/components/Settings/AccessTokenSection.tsx
+3
-3
MemberSection.tsx
web/src/components/Settings/MemberSection.tsx
+19
-9
MemoRelatedSettings.tsx
web/src/components/Settings/MemoRelatedSettings.tsx
+49
-59
MyAccountSection.tsx
web/src/components/Settings/MyAccountSection.tsx
+4
-4
PreferencesSection.tsx
web/src/components/Settings/PreferencesSection.tsx
+19
-19
SSOSection.tsx
web/src/components/Settings/SSOSection.tsx
+8
-8
StorageSection.tsx
web/src/components/Settings/StorageSection.tsx
+45
-22
UserSessionsSection.tsx
web/src/components/Settings/UserSessionsSection.tsx
+2
-2
WebhookSection.tsx
web/src/components/Settings/WebhookSection.tsx
+2
-2
WorkspaceSection.tsx
web/src/components/Settings/WorkspaceSection.tsx
+25
-20
StatCard.tsx
web/src/components/StatisticsView/StatCard.tsx
+10
-5
UpdateAccountDialog.tsx
web/src/components/UpdateAccountDialog.tsx
+8
-8
UpdateCustomizedProfileDialog.tsx
web/src/components/UpdateCustomizedProfileDialog.tsx
+7
-5
UserAvatar.tsx
web/src/components/UserAvatar.tsx
+1
-1
UserBanner.tsx
web/src/components/UserBanner.tsx
+2
-2
VisibilityIcon.tsx
web/src/components/VisibilityIcon.tsx
+1
-1
OverflowTip.tsx
web/src/components/kit/OverflowTip.tsx
+16
-7
Popover.tsx
web/src/components/ui/Popover.tsx
+28
-22
badge.tsx
web/src/components/ui/badge.tsx
+35
-0
button.tsx
web/src/components/ui/button.tsx
+47
-0
checkbox.tsx
web/src/components/ui/checkbox.tsx
+23
-0
command.tsx
web/src/components/ui/command.tsx
+111
-0
dialog.tsx
web/src/components/ui/dialog.tsx
+98
-0
input.tsx
web/src/components/ui/input.tsx
+20
-0
label.tsx
web/src/components/ui/label.tsx
+18
-0
radio-group.tsx
web/src/components/ui/radio-group.tsx
+27
-0
select.tsx
web/src/components/ui/select.tsx
+144
-0
separator.tsx
web/src/components/ui/separator.tsx
+25
-0
sheet.tsx
web/src/components/ui/sheet.tsx
+87
-0
switch.tsx
web/src/components/ui/switch.tsx
+25
-0
textarea.tsx
web/src/components/ui/textarea.tsx
+17
-0
tooltip.tsx
web/src/components/ui/tooltip.tsx
+40
-0
HomeLayout.tsx
web/src/layouts/HomeLayout.tsx
+1
-1
RootLayout.tsx
web/src/layouts/RootLayout.tsx
+1
-1
utils.ts
web/src/lib/utils.ts
+6
-0
main.tsx
web/src/main.tsx
+2
-5
Attachments.tsx
web/src/pages/Attachments.tsx
+26
-15
MemoDetail.tsx
web/src/pages/MemoDetail.tsx
+4
-4
Setting.tsx
web/src/pages/Setting.tsx
+12
-7
SignIn.tsx
web/src/pages/SignIn.tsx
+12
-6
SignUp.tsx
web/src/pages/SignUp.tsx
+5
-13
UserProfile.tsx
web/src/pages/UserProfile.tsx
+2
-2
style.css
web/src/style.css
+119
-0
index.ts
web/src/theme/index.ts
+4
-34
index.ts
web/src/utils/index.ts
+0
-1
vite.config.mts
web/vite.config.mts
+0
-1
No files found.
web/components.json
0 → 100644
View file @
493832ae
{
"$schema"
:
"https://ui.shadcn.com/schema.json"
,
"style"
:
"new-york"
,
"rsc"
:
false
,
"tsx"
:
true
,
"tailwind"
:
{
"config"
:
""
,
"css"
:
"src/style.css"
,
"baseColor"
:
"zinc"
,
"cssVariables"
:
true
,
"prefix"
:
""
},
"aliases"
:
{
"components"
:
"@/components"
,
"utils"
:
"@/lib/utils"
,
"ui"
:
"@/components/ui"
,
"lib"
:
"@/lib"
,
"hooks"
:
"@/hooks"
},
"iconLibrary"
:
"lucide"
}
\ No newline at end of file
web/package.json
View file @
493832ae
...
...
@@ -15,11 +15,20 @@
"@emotion/styled"
:
"^11.14.0"
,
"@github/relative-time-element"
:
"^4.4.8"
,
"@matejmazur/react-katex"
:
"^3.1.3"
,
"@mui/joy"
:
"5.0.0-beta.52"
,
"@radix-ui/react-checkbox"
:
"^1.3.2"
,
"@radix-ui/react-dialog"
:
"^1.1.14"
,
"@radix-ui/react-label"
:
"^2.1.7"
,
"@radix-ui/react-popover"
:
"^1.1.14"
,
"@radix-ui/react-radio-group"
:
"^1.3.7"
,
"@radix-ui/react-select"
:
"^2.2.5"
,
"@radix-ui/react-separator"
:
"^1.1.7"
,
"@radix-ui/react-slot"
:
"^1.2.3"
,
"@radix-ui/react-switch"
:
"^1.2.5"
,
"@radix-ui/react-tooltip"
:
"^1.2.7"
,
"@tailwindcss/vite"
:
"^4.1.8"
,
"
@usememos/mui"
:
"0.1.0-20250607013227
"
,
"
class-variance-authority"
:
"^0.7.1
"
,
"clsx"
:
"^2.1.1"
,
"cmdk"
:
"^1.1.1"
,
"copy-to-clipboard"
:
"^3.3.3"
,
"dayjs"
:
"^1.11.13"
,
"fuse.js"
:
"^7.1.0"
,
...
...
@@ -70,6 +79,7 @@
"nice-grpc-web"
:
"^3.3.7"
,
"prettier"
:
"^3.5.3"
,
"terser"
:
"^5.40.0"
,
"tw-animate-css"
:
"^1.3.4"
,
"typescript"
:
"^5.8.3"
,
"typescript-eslint"
:
"^8.33.0"
,
"vite"
:
"^6.3.5"
...
...
web/pnpm-lock.yaml
View file @
493832ae
This source diff could not be displayed because it is too large. You can
view the blob
instead.
web/src/App.tsx
View file @
493832ae
import
{
useColorScheme
}
from
"@mui/joy"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useEffect
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
{
Outlet
}
from
"react-router-dom"
;
import
{
getSystemColorScheme
}
from
"./helpers/utils"
;
...
...
@@ -10,7 +9,7 @@ import { userStore, workspaceStore } from "./store/v2";
const
App
=
observer
(()
=>
{
const
{
i18n
}
=
useTranslation
();
const
navigateTo
=
useNavigateTo
();
const
{
mode
,
setMode
}
=
useColorScheme
(
);
const
[
mode
,
setMode
]
=
useState
<
"light"
|
"dark"
>
(
"light"
);
const
workspaceProfile
=
workspaceStore
.
state
.
profile
;
const
userSetting
=
userStore
.
state
.
userSetting
;
const
workspaceGeneralSetting
=
workspaceStore
.
state
.
generalSetting
;
...
...
web/src/components/ActivityCalendar/ActivityCalendar.tsx
View file @
493832ae
import
{
Tooltip
}
from
"@mui/joy"
;
import
dayjs
from
"dayjs"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
memo
,
useMemo
}
from
"react"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
type
{
ActivityCalendarProps
,
CalendarDay
}
from
"@/types/statistics"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
const
getCellOpacity
=
(
ratio
:
number
):
string
=>
{
...
...
@@ -59,9 +59,16 @@ const CalendarCell = memo(
}
return
(
<
Tooltip
className=
"shrink-0"
title=
{
tooltipText
}
placement=
"top"
arrow
>
{
cellContent
}
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
div
className=
"shrink-0"
>
{
cellContent
}
</
div
>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
tooltipText
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
);
},
);
...
...
web/src/components/AppearanceSelect.tsx
View file @
493832ae
import
{
Option
,
Select
}
from
"@mui/joy"
;
import
{
SunIcon
,
MoonIcon
,
SmileIcon
}
from
"lucide-react"
;
import
{
FC
}
from
"react"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
interface
Props
{
...
...
@@ -31,21 +31,20 @@ const AppearanceSelect: FC<Props> = (props: Props) => {
};
return
(
<
Select
className=
{
`min-w-40! w-auto whitespace-nowrap ${className ?? ""}`
}
value=
{
value
}
onChange=
{
(
_
,
appearance
)
=>
{
if
(
appearance
)
{
handleSelectChange
(
appearance
);
}
}
}
startDecorator=
{
getPrefixIcon
(
value
)
}
>
{
appearanceList
.
map
((
item
)
=>
(
<
Option
key=
{
item
}
value=
{
item
}
className=
"whitespace-nowrap"
>
{
t
(
`setting.appearance-option.${item}`
)
}
</
Option
>
))
}
<
Select
value=
{
value
}
onValueChange=
{
handleSelectChange
}
>
<
SelectTrigger
className=
{
`min-w-40 w-auto whitespace-nowrap ${className ?? ""}`
}
>
<
SelectValue
placeholder=
"Select appearance"
/>
</
SelectTrigger
>
<
SelectContent
>
{
appearanceList
.
map
((
item
)
=>
(
<
SelectItem
key=
{
item
}
value=
{
item
}
className=
"whitespace-nowrap"
>
<
div
className=
"flex items-center gap-2"
>
{
getPrefixIcon
(
item
)
}
{
t
(
`setting.appearance-option.${item}`
)
}
</
div
>
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
);
};
...
...
web/src/components/AttachmentIcon.tsx
View file @
493832ae
...
...
@@ -10,8 +10,8 @@ import {
SheetIcon
,
}
from
"lucide-react"
;
import
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
getAttachmentType
,
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
import
SquareDiv
from
"./kit/SquareDiv"
;
...
...
web/src/components/AuthFooter.tsx
View file @
493832ae
import
{
observer
}
from
"mobx-react-lite"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
{
cn
}
from
"@/utils"
;
import
AppearanceSelect
from
"./AppearanceSelect"
;
import
LocaleSelect
from
"./LocaleSelect"
;
...
...
web/src/components/BrandBanner.tsx
View file @
493832ae
import
{
observer
}
from
"mobx-react-lite"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
{
cn
}
from
"@/utils"
;
import
UserAvatar
from
"./UserAvatar"
;
interface
Props
{
...
...
web/src/components/ChangeMemberPasswordDialog.tsx
View file @
493832ae
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
userStore
}
from
"@/store/v2"
;
import
{
User
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
...
...
@@ -69,7 +70,7 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
<
p
>
{
t
(
"setting.account-section.change-password"
)
}
(
{
user
.
displayName
}
)
</
p
>
<
Button
variant=
"
plain
"
onClick=
{
handleCloseBtnClick
}
>
<
Button
variant=
"
ghost
"
onClick=
{
handleCloseBtnClick
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
...
...
@@ -91,7 +92,7 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
onChange=
{
handleNewPasswordAgainChanged
}
/>
<
div
className=
"flex flex-row justify-end items-center mt-4 w-full gap-x-2"
>
<
Button
variant=
"
plain
"
onClick=
{
handleCloseBtnClick
}
>
<
Button
variant=
"
ghost
"
onClick=
{
handleCloseBtnClick
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
onClick=
{
handleSaveBtnClick
}
>
...
...
web/src/components/CreateAccessTokenDialog.tsx
View file @
493832ae
import
{
Radio
,
RadioGroup
}
from
"@mui/joy"
;
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
React
,
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useLoading
from
"@/hooks/useLoading"
;
...
...
@@ -56,9 +58,9 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
});
};
const
handleRoleInputChange
=
(
e
:
React
.
ChangeEvent
<
HTMLInputElement
>
)
=>
{
const
handleRoleInputChange
=
(
value
:
string
)
=>
{
setPartialState
({
expiration
:
Number
(
e
.
target
.
value
),
expiration
:
Number
(
value
),
});
};
...
...
@@ -89,7 +91,7 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center w-full mb-4 gap-2"
>
<
p
>
{
t
(
"setting.access-token-section.create-dialog.create-access-token"
)
}
</
p
>
<
Button
variant=
"
plain
"
onClick=
{
()
=>
destroy
()
}
>
<
Button
variant=
"
ghost
"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
...
...
@@ -113,18 +115,21 @@ const CreateAccessTokenDialog: React.FC<Props> = (props: Props) => {
{
t
(
"setting.access-token-section.create-dialog.expiration"
)
}
<
span
className=
"text-red-600"
>
*
</
span
>
</
span
>
<
div
className=
"w-full flex flex-row justify-start items-center text-base"
>
<
RadioGroup
orientation=
"horizontal"
value=
{
state
.
expiration
}
onChange=
{
handleRoleInputChange
}
>
<
RadioGroup
value=
{
state
.
expiration
.
toString
()
}
onValueChange=
{
handleRoleInputChange
}
className=
"flex flex-row gap-4"
>
{
expirationOptions
.
map
((
option
)
=>
(
<
Radio
key=
{
option
.
value
}
value=
{
option
.
value
}
checked=
{
state
.
expiration
===
option
.
value
}
label=
{
option
.
label
}
/>
<
div
key=
{
option
.
value
}
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
option
.
value
.
toString
()
}
id=
{
`expiration-${option.value}`
}
/>
<
Label
htmlFor=
{
`expiration-${option.value}`
}
>
{
option
.
label
}
</
Label
>
</
div
>
))
}
</
RadioGroup
>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center mt-4 space-x-2"
>
<
Button
variant=
"
plain
"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
<
Button
variant=
"
ghost
"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleSaveBtnClick
}
>
<
Button
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
...
...
web/src/components/CreateIdentityProviderDialog.tsx
View file @
493832ae
import
{
Divider
,
Option
,
Select
,
Typography
}
from
"@mui/joy"
;
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
absolutifyLink
}
from
"@/helpers/utils"
;
import
{
FieldMapping
,
IdentityProvider
,
IdentityProvider_Type
,
OAuth2Config
}
from
"@/types/proto/api/v1/idp_service"
;
...
...
@@ -245,42 +247,48 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
>
{
t
(
isCreating
?
"setting.sso-section.create-sso"
:
"setting.sso-section.update-sso"
)
}
</
p
>
<
Button
variant=
"
plain
"
onClick=
{
handleCloseBtnClick
}
>
<
Button
variant=
"
ghost
"
onClick=
{
handleCloseBtnClick
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
<
div
className=
"flex flex-col justify-start items-start w-80"
>
{
isCreating
&&
(
<>
<
Typography
className=
"mb-1!"
level=
"body-md"
>
{
t
(
"common.type"
)
}
</
Typography
>
<
Select
className=
"w-full mb-4"
value=
{
type
}
onChange=
{
(
_
,
e
)
=>
setType
(
e
??
type
)
}
>
{
identityProviderTypes
.
map
((
kind
)
=>
(
<
Option
key=
{
kind
}
value=
{
kind
}
>
{
kind
}
</
Option
>
))
}
<
p
className=
"mb-1!"
>
{
t
(
"common.type"
)
}
</
p
>
<
Select
value=
{
String
(
type
)
}
onValueChange=
{
(
value
)
=>
setType
(
parseInt
(
value
)
as
unknown
as
IdentityProvider_Type
)
}
>
<
SelectTrigger
className=
"w-full mb-4"
>
<
SelectValue
/>
</
SelectTrigger
>
<
SelectContent
>
{
identityProviderTypes
.
map
((
kind
)
=>
(
<
SelectItem
key=
{
kind
}
value=
{
String
(
kind
)
}
>
{
IdentityProvider_Type
[
kind
]
||
kind
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
<
Typography
className=
"mb-2"
level=
"body-md"
>
{
t
(
"setting.sso-section.template"
)
}
</
Typography
>
<
Select
className=
"mb-1 h-auto w-full"
value=
{
selectedTemplate
}
onChange=
{
(
_
,
e
)
=>
setSelectedTemplate
(
e
??
selectedTemplate
)
}
>
{
templateList
.
map
((
template
)
=>
(
<
Option
key=
{
template
.
title
}
value=
{
template
.
title
}
>
{
template
.
title
}
</
Option
>
))
}
<
p
className=
"mb-2 text-sm font-medium"
>
{
t
(
"setting.sso-section.template"
)
}
</
p
>
<
Select
value=
{
selectedTemplate
}
onValueChange=
{
(
value
)
=>
setSelectedTemplate
(
value
)
}
>
<
SelectTrigger
className=
"mb-1 h-auto w-full"
>
<
SelectValue
/>
</
SelectTrigger
>
<
SelectContent
>
{
templateList
.
map
((
template
)
=>
(
<
SelectItem
key=
{
template
.
title
}
value=
{
template
.
title
}
>
{
template
.
title
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
<
Divider
className=
"my-2!
"
/>
<
Separator
className=
"my-2
"
/>
</>
)
}
<
Typography
className=
"mb-1!"
level=
"body-md
"
>
<
p
className=
"mb-1 text-sm font-medium
"
>
{
t
(
"common.name"
)
}
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"common.name"
)
}
value=
{
basicInfo
.
title
}
onChange=
{
(
e
)
=>
...
...
@@ -289,13 +297,10 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
title
:
e
.
target
.
value
,
})
}
fullWidth
/>
<
Typography
className=
"mb-1!"
level=
"body-md"
>
{
t
(
"setting.sso-section.identifier-filter"
)
}
</
Typography
>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.identifier-filter"
)
}
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"setting.sso-section.identifier-filter"
)
}
value=
{
basicInfo
.
identifierFilter
}
onChange=
{
(
e
)
=>
...
...
@@ -304,9 +309,8 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
identifierFilter
:
e
.
target
.
value
,
})
}
fullWidth
/>
<
Divider
className=
"my-2!
"
/>
<
Separator
className=
"my-2
"
/>
{
type
===
"OAUTH2"
&&
(
<>
{
isCreating
&&
(
...
...
@@ -314,129 +318,113 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
{
t
(
"setting.sso-section.redirect-url"
)
}
:
{
absolutifyLink
(
"/auth/callback"
)
}
</
p
>
)
}
<
Typography
className=
"mb-1!"
level=
"body-md
"
>
<
p
className=
"mb-1 text-sm font-medium
"
>
{
t
(
"setting.sso-section.client-id"
)
}
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"setting.sso-section.client-id"
)
}
value=
{
oauth2Config
.
clientId
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
clientId
:
e
.
target
.
value
})
}
fullWidth
/>
<
Typography
className=
"mb-1!"
level=
"body-md
"
>
<
p
className=
"mb-1 text-sm font-medium
"
>
{
t
(
"setting.sso-section.client-secret"
)
}
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"setting.sso-section.client-secret"
)
}
value=
{
oauth2Config
.
clientSecret
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
clientSecret
:
e
.
target
.
value
})
}
fullWidth
/>
<
Typography
className=
"mb-1!"
level=
"body-md
"
>
<
p
className=
"mb-1 text-sm font-medium
"
>
{
t
(
"setting.sso-section.authorization-endpoint"
)
}
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"setting.sso-section.authorization-endpoint"
)
}
value=
{
oauth2Config
.
authUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
authUrl
:
e
.
target
.
value
})
}
fullWidth
/>
<
Typography
className=
"mb-1!"
level=
"body-md
"
>
<
p
className=
"mb-1 text-sm font-medium
"
>
{
t
(
"setting.sso-section.token-endpoint"
)
}
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"setting.sso-section.token-endpoint"
)
}
value=
{
oauth2Config
.
tokenUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
tokenUrl
:
e
.
target
.
value
})
}
fullWidth
/>
<
Typography
className=
"mb-1!"
level=
"body-md
"
>
<
p
className=
"mb-1 text-sm font-medium
"
>
{
t
(
"setting.sso-section.user-endpoint"
)
}
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"setting.sso-section.user-endpoint"
)
}
value=
{
oauth2Config
.
userInfoUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
userInfoUrl
:
e
.
target
.
value
})
}
fullWidth
/>
<
Typography
className=
"mb-1!"
level=
"body-md
"
>
<
p
className=
"mb-1 text-sm font-medium
"
>
{
t
(
"setting.sso-section.scopes"
)
}
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"setting.sso-section.scopes"
)
}
value=
{
oauth2Scopes
}
onChange=
{
(
e
)
=>
setOAuth2Scopes
(
e
.
target
.
value
)
}
fullWidth
/>
<
Divider
className=
"my-2!
"
/>
<
Typography
className=
"mb-1!"
level=
"body-md
"
>
<
Separator
className=
"my-2
"
/>
<
p
className=
"mb-1 text-sm font-medium
"
>
{
t
(
"setting.sso-section.identifier"
)
}
<
span
className=
"text-red-600"
>
*
</
span
>
</
Typography
>
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"setting.sso-section.identifier"
)
}
value=
{
oauth2Config
.
fieldMapping
!
.
identifier
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
identifier
:
e
.
target
.
value
}
as
FieldMapping
})
}
fullWidth
/>
<
Typography
className=
"mb-1!"
level=
"body-md"
>
{
t
(
"setting.sso-section.display-name"
)
}
</
Typography
>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"setting.sso-section.display-name"
)
}
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"setting.sso-section.display-name"
)
}
value=
{
oauth2Config
.
fieldMapping
!
.
displayName
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
displayName
:
e
.
target
.
value
}
as
FieldMapping
})
}
fullWidth
/>
<
Typography
className=
"mb-1!"
level=
"body-md"
>
{
t
(
"common.email"
)
}
</
Typography
>
<
p
className=
"mb-1 text-sm font-medium"
>
{
t
(
"common.email"
)
}
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
t
(
"common.email"
)
}
value=
{
oauth2Config
.
fieldMapping
!
.
email
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
email
:
e
.
target
.
value
}
as
FieldMapping
})
}
fullWidth
/>
<
Typography
className=
"mb-1!"
level=
"body-md"
>
Avatar URL
</
Typography
>
<
p
className=
"mb-1 text-sm font-medium"
>
Avatar URL
</
p
>
<
Input
className=
"mb-2"
className=
"mb-2
w-full
"
placeholder=
{
"Avatar URL"
}
value=
{
oauth2Config
.
fieldMapping
!
.
avatarUrl
}
onChange=
{
(
e
)
=>
setPartialOAuth2Config
({
fieldMapping
:
{
...
oauth2Config
.
fieldMapping
,
avatarUrl
:
e
.
target
.
value
}
as
FieldMapping
})
}
fullWidth
/>
</>
)
}
<
div
className=
"mt-2 w-full flex flex-row justify-end items-center space-x-1"
>
<
Button
variant=
"
plain
"
onClick=
{
handleCloseBtnClick
}
>
<
Button
variant=
"
ghost
"
onClick=
{
handleCloseBtnClick
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
onClick=
{
handleConfirmBtnClick
}
disabled=
{
!
allowConfirmAction
()
}
>
<
Button
onClick=
{
handleConfirmBtnClick
}
disabled=
{
!
allowConfirmAction
()
}
>
{
t
(
isCreating
?
"common.create"
:
"common.update"
)
}
</
Button
>
</
div
>
...
...
web/src/components/CreateShortcutDialog.tsx
View file @
493832ae
import
{
Input
,
Textarea
,
Button
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
React
,
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
shortcutServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useLoading
from
"@/hooks/useLoading"
;
...
...
@@ -74,7 +76,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
`${isCreating ? t("common.create") : t("common.edit")} ${t("common.shortcuts")}`
}
</
p
>
<
Button
variant=
"
plain
"
onClick=
{
()
=>
destroy
()
}
>
<
Button
variant=
"
ghost
"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
...
...
@@ -84,8 +86,8 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
<
Input
className=
"w-full"
type=
"text"
placeholder=
""
value=
{
shortcut
.
title
}
onChange=
{
onShortcutTitleChange
}
/>
<
span
className=
"text-sm whitespace-nowrap mt-3 mb-1"
>
{
t
(
"common.filter"
)
}
</
span
>
<
Textarea
className=
"w-full"
rows=
{
3
}
fullWidth
placeholder=
{
t
(
"common.shortcut-filter"
)
}
value=
{
shortcut
.
filter
}
onChange=
{
onShortcutFilterChange
}
...
...
@@ -115,7 +117,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
</
ul
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center space-x-2 mt-2"
>
<
Button
variant=
"
plain
"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
<
Button
variant=
"
ghost
"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
...
...
web/src/components/CreateUserDialog.tsx
View file @
493832ae
import
{
Radio
,
RadioGroup
}
from
"@mui/joy"
;
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
User
,
User_Role
}
from
"@/types/proto/api/v1/user_service"
;
...
...
@@ -66,7 +68,7 @@ const CreateUserDialog: React.FC<Props> = (props: Props) => {
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
`${isCreating ? t("common.create") : t("common.edit")} ${t("common.user")}`
}
</
p
>
<
Button
variant=
"
plain
"
onClick=
{
()
=>
destroy
()
}
>
<
Button
variant=
"
ghost
"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
...
...
@@ -99,16 +101,22 @@ const CreateUserDialog: React.FC<Props> = (props: Props) => {
/>
<
span
className=
"text-sm whitespace-nowrap mt-3 mb-1"
>
{
t
(
"common.role"
)
}
</
span
>
<
RadioGroup
orientation=
"horizontal"
defaultValue=
{
user
.
role
}
onChange=
{
(
e
)
=>
setPartialUser
({
role
:
e
.
target
.
value
as
User_Role
})
}
value=
{
user
.
role
}
onValueChange=
{
(
value
)
=>
setPartialUser
({
role
:
value
as
User_Role
})
}
className=
"flex flex-row gap-4"
>
<
Radio
value=
{
User_Role
.
USER
}
label=
{
t
(
"setting.member-section.user"
)
}
/>
<
Radio
value=
{
User_Role
.
ADMIN
}
label=
{
t
(
"setting.member-section.admin"
)
}
/>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
User_Role
.
USER
}
id=
"user"
/>
<
Label
htmlFor=
"user"
>
{
t
(
"setting.member-section.user"
)
}
</
Label
>
</
div
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
User_Role
.
ADMIN
}
id=
"admin"
/>
<
Label
htmlFor=
"admin"
>
{
t
(
"setting.member-section.admin"
)
}
</
Label
>
</
div
>
</
RadioGroup
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center space-x-2 mt-2"
>
<
Button
variant=
"
plain
"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
<
Button
variant=
"
ghost
"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
...
...
web/src/components/CreateWebhookDialog.tsx
View file @
493832ae
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
webhookServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useLoading
from
"@/hooks/useLoading"
;
...
...
@@ -108,7 +109,7 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
<
p
className=
"title-text"
>
{
isCreating
?
t
(
"setting.webhook-section.create-dialog.create-webhook"
)
:
t
(
"setting.webhook-section.create-dialog.edit-webhook"
)
}
</
p
>
<
Button
variant=
"
plain
"
onClick=
{
()
=>
destroy
()
}
>
<
Button
variant=
"
ghost
"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
...
...
@@ -142,7 +143,7 @@ const CreateWebhookDialog: React.FC<Props> = (props: Props) => {
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center mt-2 space-x-2"
>
<
Button
variant=
"
plain
"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
<
Button
variant=
"
ghost
"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleSaveBtnClick
}
>
...
...
web/src/components/DateTimeInput.tsx
View file @
493832ae
import
dayjs
from
"dayjs"
;
import
toast
from
"react-hot-toast"
;
import
{
cn
}
from
"@/utils"
;
import
{
cn
}
from
"@/
lib/
utils"
;
const
DATE_TIME_FORMAT
=
"M/D/YYYY, H:mm:ss"
;
...
...
web/src/components/Dialog/BaseDialog.tsx
View file @
493832ae
import
{
CssVarsProvider
}
from
"@mui/joy"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useEffect
,
useRef
}
from
"react"
;
import
{
createRoot
}
from
"react-dom/client"
;
import
{
cn
}
from
"@/lib/utils"
;
import
dialogStore
from
"@/store/v2/dialog"
;
import
theme
from
"@/theme"
;
import
{
cn
}
from
"@/utils"
;
interface
DialogConfig
{
dialogName
:
string
;
...
...
@@ -90,11 +88,9 @@ export function generateDialog<T extends DialogProps>(
}
as
T
;
const
Fragment
=
observer
(()
=>
(
<
CssVarsProvider
theme=
{
theme
}
>
<
BaseDialog
destroy=
{
cbs
.
destroy
}
clickSpaceDestroy=
{
true
}
{
...
config
}
>
<
DialogComponent
{
...
dialogProps
}
/>
</
BaseDialog
>
</
CssVarsProvider
>
<
BaseDialog
destroy=
{
cbs
.
destroy
}
clickSpaceDestroy=
{
true
}
{
...
config
}
>
<
DialogComponent
{
...
dialogProps
}
/>
</
BaseDialog
>
));
dialog
.
render
(<
Fragment
/>);
...
...
web/src/components/HomeSidebar/HomeSidebar.tsx
View file @
493832ae
...
...
@@ -4,9 +4,9 @@ import { matchPath, useLocation } from "react-router-dom";
import
useDebounce
from
"react-use/lib/useDebounce"
;
import
SearchBar
from
"@/components/SearchBar"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Routes
}
from
"@/router"
;
import
{
memoStore
,
userStore
}
from
"@/store/v2"
;
import
{
cn
}
from
"@/utils"
;
import
MemoFilters
from
"../MemoFilters"
;
import
StatisticsView
from
"../StatisticsView"
;
import
ShortcutsSection
from
"./ShortcutsSection"
;
...
...
web/src/components/HomeSidebar/HomeSidebarDrawer.tsx
View file @
493832ae
import
{
Drawer
}
from
"@mui/joy"
;
import
{
Button
}
from
"@usememos/mui"
;
import
{
MenuIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Sheet
,
SheetContent
,
SheetTrigger
}
from
"@/components/ui/sheet"
;
import
HomeSidebar
from
"./HomeSidebar"
;
const
HomeSidebarDrawer
=
()
=>
{
...
...
@@ -13,25 +13,17 @@ const HomeSidebarDrawer = () => {
setOpen
(
false
);
},
[
location
.
pathname
]);
const
toggleDrawer
=
(
inOpen
:
boolean
)
=>
(
event
:
React
.
KeyboardEvent
|
React
.
MouseEvent
)
=>
{
if
(
event
.
type
===
"keydown"
&&
((
event
as
React
.
KeyboardEvent
).
key
===
"Tab"
||
(
event
as
React
.
KeyboardEvent
).
key
===
"Shift"
))
{
return
;
}
setOpen
(
inOpen
);
};
return
(
<>
<
Button
variant=
"plain"
className=
"bg-transparent! px-2"
onClick=
{
toggleDrawer
(
true
)
}
>
<
MenuIcon
className=
"w-6 h-auto dark:text-gray-400"
/
>
</
Button
>
<
Drawer
anchor=
"right"
size=
"sm"
open=
{
open
}
onClose=
{
toggleDrawer
(
false
)
}
>
<
div
className=
"w-full h-full bg-zinc-100 dark:bg-zinc-900"
>
<
HomeSidebar
className=
"px-4 py-4"
/
>
<
/
div
>
</
Drawer
>
</>
<
Sheet
open=
{
open
}
onOpenChange=
{
setOpen
}
>
<
SheetTrigger
asChild
>
<
Button
variant=
"ghost"
className=
"bg-transparent! px-2"
>
<
MenuIcon
className=
"w-6 h-auto dark:text-gray-400"
/
>
</
Button
>
</
SheetTrigger
>
<
SheetContent
side=
"right"
className=
"w-full sm:w-80 bg-zinc-100 dark:bg-zinc-900"
>
<
HomeSidebar
className=
"px-4 py-4"
/
>
</
SheetContent
>
</
Sheet
>
);
};
...
...
web/src/components/HomeSidebar/ShortcutsSection.tsx
View file @
493832ae
import
{
Tooltip
}
from
"@mui/joy"
;
import
{
Edit3Icon
,
MoreVerticalIcon
,
TrashIcon
,
PlusIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
shortcutServiceClient
}
from
"@/grpcweb"
;
import
useAsyncEffect
from
"@/hooks/useAsyncEffect"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
userStore
}
from
"@/store/v2"
;
import
memoFilterStore
from
"@/store/v2/memoFilter"
;
import
{
Shortcut
}
from
"@/types/proto/api/v1/shortcut_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
showCreateShortcutDialog
from
"../CreateShortcutDialog"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/
p
opover"
;
const
emojiRegex
=
/^
(\p
{Emoji_Presentation}|
\p
{Emoji}
\u
FE0F
)
$/
u
;
...
...
@@ -40,9 +40,16 @@ const ShortcutsSection = observer(() => {
<
div
className=
"w-full flex flex-col justify-start items-start mt-3 px-1 h-auto shrink-0 flex-nowrap hide-scrollbar"
>
<
div
className=
"flex flex-row justify-between items-center w-full gap-1 mb-1 text-sm leading-6 text-gray-400 select-none"
>
<
span
>
{
t
(
"common.shortcuts"
)
}
</
span
>
<
Tooltip
title=
{
t
(
"common.create"
)
}
placement=
"top"
>
<
PlusIcon
className=
"w-4 h-auto"
onClick=
{
()
=>
showCreateShortcutDialog
({})
}
/>
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
PlusIcon
className=
"w-4 h-auto cursor-pointer"
onClick=
{
()
=>
showCreateShortcutDialog
({})
}
/>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
t
(
"common.create"
)
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
</
div
>
<
div
className=
"w-full flex flex-row justify-start items-center relative flex-wrap gap-x-2 gap-y-1"
>
{
shortcuts
.
map
((
shortcut
)
=>
{
...
...
web/src/components/HomeSidebar/TagsSection.tsx
View file @
493832ae
import
{
Switch
}
from
"@usememos/mui"
;
import
{
Edit3Icon
,
HashIcon
,
MoreVerticalIcon
,
TagsIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
toast
from
"react-hot-toast"
;
import
useLocalStorage
from
"react-use/lib/useLocalStorage"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
userStore
}
from
"@/store/v2"
;
import
memoFilterStore
,
{
MemoFilter
}
from
"@/store/v2/memoFilter"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
showRenameTagDialog
from
"../RenameTagDialog"
;
import
TagTree
from
"../TagTree"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/
p
opover"
;
interface
Props
{
readonly
?:
boolean
;
...
...
@@ -58,7 +58,7 @@ const TagsSection = observer((props: Props) => {
<
PopoverContent
align=
"end"
alignOffset=
{
-
12
}
>
<
div
className=
"w-auto flex flex-row justify-between items-center gap-2 p-1"
>
<
span
className=
"text-sm shrink-0 dark:text-zinc-400"
>
{
t
(
"common.tree-mode"
)
}
</
span
>
<
Switch
size=
"sm"
checked=
{
treeMode
}
onChange=
{
(
event
)
=>
setTreeMode
(
event
.
target
.
checked
)
}
/>
<
Switch
checked=
{
treeMode
}
onCheckedChange=
{
(
checked
)
=>
setTreeMode
(
checked
)
}
/>
</
div
>
</
PopoverContent
>
</
Popover
>
...
...
web/src/components/Inbox/MemoCommentMessage.tsx
View file @
493832ae
import
{
Tooltip
}
from
"@mui/joy"
;
import
{
InboxIcon
,
LoaderIcon
,
MessageCircleIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useState
}
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
activityServiceClient
}
from
"@/grpcweb"
;
import
useAsyncEffect
from
"@/hooks/useAsyncEffect"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
activityNamePrefix
}
from
"@/store/common"
;
import
{
memoStore
,
userStore
}
from
"@/store/v2"
;
import
{
Inbox
,
Inbox_Status
}
from
"@/types/proto/api/v1/inbox_service"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
User
}
from
"@/types/proto/api/v1/user_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
interface
Props
{
...
...
@@ -79,9 +79,16 @@ const MemoCommentMessage = observer(({ inbox }: Props) => {
:
"border-gray-500 text-gray-500 bg-gray-50 dark:bg-zinc-800"
,
)
}
>
<
Tooltip
title=
{
"Comment"
}
placement=
"bottom"
>
<
MessageCircleIcon
className=
"w-4 sm:w-5 h-auto"
/>
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
>
<
MessageCircleIcon
className=
"w-4 sm:w-5 h-auto"
/>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
Comment
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
</
div
>
<
div
className=
{
cn
(
...
...
@@ -95,12 +102,19 @@ const MemoCommentMessage = observer(({ inbox }: Props) => {
<
span
className=
"text-sm text-gray-500"
>
{
inbox
.
createTime
?.
toLocaleString
()
}
</
span
>
<
div
>
{
inbox
.
status
===
Inbox_Status
.
UNREAD
&&
(
<
Tooltip
title=
{
t
(
"common.archive"
)
}
placement=
"top"
>
<
InboxIcon
className=
"w-4 h-auto cursor-pointer text-gray-400 hover:text-blue-600"
onClick=
{
()
=>
handleArchiveMessage
()
}
/>
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
>
<
InboxIcon
className=
"w-4 h-auto cursor-pointer text-gray-400 hover:text-blue-600"
onClick=
{
()
=>
handleArchiveMessage
()
}
/>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
t
(
"common.archive"
)
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
)
}
</
div
>
</
div
>
...
...
web/src/components/LearnMore.tsx
View file @
493832ae
import
{
Tooltip
}
from
"@mui/joy"
;
import
{
ExternalLinkIcon
}
from
"lucide-react"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
interface
Props
{
...
...
@@ -13,11 +13,18 @@ const LearnMore: React.FC<Props> = (props: Props) => {
const
t
=
useTranslate
();
return
(
<
Tooltip
title=
{
title
??
t
(
"common.learn-more"
)
}
placement=
"top"
>
<
a
className=
{
`text-gray-500 dark:text-gray-400 hover:text-blue-600 ${className}`
}
href=
{
url
}
target=
"_blank"
>
<
ExternalLinkIcon
className=
"w-4 h-auto"
/>
</
a
>
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
a
className=
{
`text-gray-500 dark:text-gray-400 hover:text-blue-600 ${className}`
}
href=
{
url
}
target=
"_blank"
>
<
ExternalLinkIcon
className=
"w-4 h-auto"
/>
</
a
>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
title
??
t
(
"common.learn-more"
)
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
);
};
...
...
web/src/components/LocaleSelect.tsx
View file @
493832ae
import
{
Option
,
Select
}
from
"@mui/joy"
;
import
{
GlobeIcon
}
from
"lucide-react"
;
import
{
FC
}
from
"react"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
locales
}
from
"@/i18n"
;
interface
Props
{
...
...
@@ -17,32 +17,35 @@ const LocaleSelect: FC<Props> = (props: Props) => {
};
return
(
<
Select
className=
{
`min-w-40! w-auto whitespace-nowrap ${className ?? ""}`
}
startDecorator=
{
<
GlobeIcon
className=
"w-4 h-auto"
/>
}
value=
{
value
}
onChange=
{
(
_
,
value
)
=>
handleSelectChange
(
value
as
Locale
)
}
>
{
locales
.
map
((
locale
)
=>
{
try
{
const
languageName
=
new
Intl
.
DisplayNames
([
locale
],
{
type
:
"language"
}).
of
(
locale
);
if
(
languageName
)
{
return
(
<
Option
key=
{
locale
}
value=
{
locale
}
>
{
languageName
.
charAt
(
0
).
toUpperCase
()
+
languageName
.
slice
(
1
)
}
</
Option
>
);
<
Select
value=
{
value
}
onValueChange=
{
handleSelectChange
}
>
<
SelectTrigger
className=
{
`min-w-40 w-auto whitespace-nowrap ${className ?? ""}`
}
>
<
div
className=
"flex items-center gap-2"
>
<
GlobeIcon
className=
"w-4 h-auto"
/>
<
SelectValue
placeholder=
"Select language"
/>
</
div
>
</
SelectTrigger
>
<
SelectContent
>
{
locales
.
map
((
locale
)
=>
{
try
{
const
languageName
=
new
Intl
.
DisplayNames
([
locale
],
{
type
:
"language"
}).
of
(
locale
);
if
(
languageName
)
{
return
(
<
SelectItem
key=
{
locale
}
value=
{
locale
}
>
{
languageName
.
charAt
(
0
).
toUpperCase
()
+
languageName
.
slice
(
1
)
}
</
SelectItem
>
);
}
}
catch
{
// do nth
}
}
catch
{
// do nth
}
return
(
<
Option
key=
{
locale
}
value=
{
locale
}
>
{
locale
}
</
Option
>
);
})
}
return
(
<
SelectItem
key=
{
locale
}
value=
{
locale
}
>
{
locale
}
</
SelectItem
>
);
})
}
</
SelectContent
>
</
Select
>
);
};
...
...
web/src/components/MasonryView/MasonryView.tsx
View file @
493832ae
import
{
useCallback
,
useEffect
,
useRef
,
useState
}
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
interface
Props
{
memoList
:
Memo
[];
...
...
web/src/components/MemoActionMenu.tsx
View file @
493832ae
...
...
@@ -15,13 +15,13 @@ import toast from "react-hot-toast";
import
{
useLocation
}
from
"react-router-dom"
;
import
{
markdownServiceClient
}
from
"@/grpcweb"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
memoStore
,
userStore
}
from
"@/store/v2"
;
import
{
State
}
from
"@/types/proto/api/v1/common"
;
import
{
NodeType
}
from
"@/types/proto/api/v1/markdown_service"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"./ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"./ui/
p
opover"
;
interface
Props
{
memo
:
Memo
;
...
...
web/src/components/MemoAttachmentListView.tsx
View file @
493832ae
import
{
memo
}
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
getAttachmentType
,
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
MemoAttachment
from
"./MemoAttachment"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
...
...
web/src/components/MemoContent/CodeBlock.tsx
View file @
493832ae
...
...
@@ -3,7 +3,7 @@ import hljs from "highlight.js";
import
{
CopyIcon
}
from
"lucide-react"
;
import
{
useCallback
,
useMemo
}
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
{
cn
}
from
"@/utils"
;
import
{
cn
}
from
"@/
lib/
utils"
;
import
MermaidBlock
from
"./MermaidBlock"
;
import
{
BaseProps
}
from
"./types"
;
...
...
web/src/components/MemoContent/EmbeddedContent/EmbeddedAttachment.tsx
View file @
493832ae
...
...
@@ -2,8 +2,8 @@ import { observer } from "mobx-react-lite";
import
{
useEffect
}
from
"react"
;
import
MemoAttachmentListView
from
"@/components/MemoAttachmentListView"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
attachmentStore
}
from
"@/store/v2"
;
import
{
cn
}
from
"@/utils"
;
import
Error
from
"./Error"
;
interface
Props
{
...
...
web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx
View file @
493832ae
...
...
@@ -6,9 +6,9 @@ import toast from "react-hot-toast";
import
{
Link
}
from
"react-router-dom"
;
import
MemoAttachmentListView
from
"@/components/MemoAttachmentListView"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
extractMemoIdFromName
}
from
"@/store/common"
;
import
{
memoStore
}
from
"@/store/v2"
;
import
{
cn
}
from
"@/utils"
;
import
MemoContent
from
".."
;
import
{
RendererContext
}
from
"../types"
;
import
Error
from
"./Error"
;
...
...
web/src/components/MemoContent/HorizontalRule.tsx
View file @
493832ae
import
{
Divider
}
from
"@mui/joy
"
;
import
{
Separator
}
from
"@/components/ui/separator
"
;
import
{
BaseProps
}
from
"./types"
;
interface
Props
extends
BaseProps
{
...
...
@@ -6,7 +6,7 @@ interface Props extends BaseProps {
}
const
HorizontalRule
:
React
.
FC
<
Props
>
=
()
=>
{
return
<
Divide
r
className=
"my-3!"
/>;
return
<
Separato
r
className=
"my-3!"
/>;
};
export
default
HorizontalRule
;
web/src/components/MemoContent/Link.tsx
View file @
493832ae
import
{
Link
as
MLink
,
Tooltip
}
from
"@mui/joy"
;
import
{
useState
}
from
"react"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
markdownServiceClient
}
from
"@/grpcweb"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
{
LinkMetadata
,
Node
}
from
"@/types/proto/api/v1/markdown_service"
;
...
...
@@ -43,33 +43,38 @@ const Link: React.FC<Props> = ({ content, url }: Props) => {
};
return
(
<
Tooltip
variant=
"outlined"
title=
{
linkMetadata
&&
(
<
div
className=
"w-full max-w-64 sm:max-w-96 p-1 flex flex-col"
>
<
div
className=
"w-full flex flex-row justify-start items-center gap-1"
>
<
img
className=
"w-5 h-5 rounded"
src=
{
getFaviconWithGoogleS2
(
url
)
}
alt=
{
linkMetadata
?.
title
}
/>
<
h3
className=
"text-base truncate dark:opacity-90"
>
{
linkMetadata
?.
title
}
</
h3
>
<
TooltipProvider
>
<
Tooltip
open=
{
showTooltip
}
>
<
TooltipTrigger
asChild
>
<
a
className=
"underline text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
target=
"_blank"
href=
{
url
}
rel=
"noopener noreferrer"
onMouseEnter=
{
handleMouseEnter
}
onMouseLeave=
{
()
=>
setShowTooltip
(
false
)
}
>
{
content
?
content
.
map
((
child
,
index
)
=>
<
Renderer
key=
{
`${child.type}-${index}`
}
index=
{
String
(
index
)
}
node=
{
child
}
/>)
:
url
}
</
a
>
</
TooltipTrigger
>
{
linkMetadata
&&
(
<
TooltipContent
className=
"w-full max-w-64 sm:max-w-96 p-1"
>
<
div
className=
"w-full flex flex-col"
>
<
div
className=
"w-full flex flex-row justify-start items-center gap-1"
>
<
img
className=
"w-5 h-5 rounded"
src=
{
getFaviconWithGoogleS2
(
url
)
}
alt=
{
linkMetadata
?.
title
}
/>
<
h3
className=
"text-base truncate dark:opacity-90"
>
{
linkMetadata
?.
title
}
</
h3
>
</
div
>
{
linkMetadata
.
description
&&
(
<
p
className=
"mt-1 w-full text-sm leading-snug opacity-80 line-clamp-3"
>
{
linkMetadata
.
description
}
</
p
>
)
}
{
linkMetadata
.
image
&&
(
<
img
className=
"mt-1 w-full h-32 object-cover rounded"
src=
{
linkMetadata
.
image
}
alt=
{
linkMetadata
.
title
}
/>
)
}
</
div
>
{
linkMetadata
.
description
&&
(
<
p
className=
"mt-1 w-full text-sm leading-snug opacity-80 line-clamp-3"
>
{
linkMetadata
.
description
}
</
p
>
)
}
{
linkMetadata
.
image
&&
(
<
img
className=
"mt-1 w-full h-32 object-cover rounded"
src=
{
linkMetadata
.
image
}
alt=
{
linkMetadata
.
title
}
/>
)
}
</
div
>
)
}
open=
{
showTooltip
}
arrow
>
<
MLink
underline=
"always"
target=
"_blank"
href=
{
url
}
rel=
"noopener noreferrer"
>
<
span
onMouseEnter=
{
handleMouseEnter
}
onMouseLeave=
{
()
=>
setShowTooltip
(
false
)
}
>
{
content
?
content
.
map
((
child
,
index
)
=>
<
Renderer
key=
{
`${child.type}-${index}`
}
index=
{
String
(
index
)
}
node=
{
child
}
/>)
:
url
}
</
span
>
</
MLink
>
</
Tooltip
>
</
TooltipContent
>
)
}
</
Tooltip
>
</
TooltipProvider
>
);
};
...
...
web/src/components/MemoContent/List.tsx
View file @
493832ae
import
{
head
}
from
"lodash-es"
;
import
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
ListNode_Kind
,
Node
,
NodeType
}
from
"@/types/proto/api/v1/markdown_service"
;
import
{
cn
}
from
"@/utils"
;
import
Renderer
from
"./Renderer"
;
interface
Props
{
...
...
web/src/components/MemoContent/Math.tsx
View file @
493832ae
import
TeX
from
"@matejmazur/react-katex"
;
import
{
cn
}
from
"@/utils"
;
import
{
cn
}
from
"@/
lib/
utils"
;
import
"katex/dist/katex.min.css"
;
interface
Props
{
...
...
web/src/components/MemoContent/MermaidBlock.tsx
View file @
493832ae
import
{
useColorScheme
}
from
"@mui/joy"
;
import
{
useEffect
,
useRef
}
from
"react"
;
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
interface
Props
{
content
:
string
;
}
const
MermaidBlock
:
React
.
FC
<
Props
>
=
({
content
}:
Props
)
=>
{
const
{
mode
:
colorMode
}
=
useColorScheme
(
);
const
[
colorMode
,
setColorMode
]
=
useState
<
"light"
|
"dark"
>
(
"light"
);
const
mermaidDockBlock
=
useRef
<
null
>
(
null
);
// Simple dark mode detection
useEffect
(()
=>
{
const
updateMode
=
()
=>
{
const
isDark
=
document
.
documentElement
.
classList
.
contains
(
"dark"
);
setColorMode
(
isDark
?
"dark"
:
"light"
);
};
updateMode
();
// Watch for changes to the dark class
const
observer
=
new
MutationObserver
(
updateMode
);
observer
.
observe
(
document
.
documentElement
,
{
attributes
:
true
,
attributeFilter
:
[
"class"
],
});
return
()
=>
observer
.
disconnect
();
},
[]);
useEffect
(()
=>
{
// Dynamically import mermaid to ensure compatibility with Vite
const
initializeMermaid
=
async
()
=>
{
...
...
web/src/components/MemoContent/Spoiler.tsx
View file @
493832ae
import
{
useState
}
from
"react"
;
import
{
cn
}
from
"@/utils"
;
import
{
cn
}
from
"@/
lib/
utils"
;
interface
Props
{
content
:
string
;
...
...
web/src/components/MemoContent/Tag.tsx
View file @
493832ae
...
...
@@ -2,10 +2,10 @@ import { observer } from "mobx-react-lite";
import
{
useContext
}
from
"react"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Routes
}
from
"@/router"
;
import
{
memoFilterStore
}
from
"@/store/v2"
;
import
{
stringifyFilters
,
MemoFilter
}
from
"@/store/v2/memoFilter"
;
import
{
cn
}
from
"@/utils"
;
import
{
RendererContext
}
from
"./types"
;
interface
Props
{
...
...
web/src/components/MemoContent/TaskListItem.tsx
View file @
493832ae
import
{
Checkbox
}
from
"@usememos/mui"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useContext
}
from
"react"
;
import
{
Checkbox
}
from
"@/components/ui/checkbox"
;
import
{
markdownServiceClient
}
from
"@/grpcweb"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
memoStore
}
from
"@/store/v2"
;
import
{
Node
,
TaskListItemNode
}
from
"@/types/proto/api/v1/markdown_service"
;
import
{
cn
}
from
"@/utils"
;
import
Renderer
from
"./Renderer"
;
import
{
RendererContext
}
from
"./types"
;
...
...
@@ -39,7 +39,12 @@ const TaskListItem = observer(({ node, complete, children }: Props) => {
return
(
<
li
className=
{
cn
(
"w-full grid grid-cols-[24px_1fr]"
)
}
>
<
span
className=
"w-6 h-6 flex justify-start items-center"
>
<
Checkbox
size=
"sm"
checked=
{
complete
}
disabled=
{
context
.
readonly
}
onChange=
{
(
e
)
=>
handleCheckboxChange
(
e
.
target
.
checked
)
}
/>
<
Checkbox
className=
"h-4 w-4"
checked=
{
complete
}
disabled=
{
context
.
readonly
}
onCheckedChange=
{
(
checked
)
=>
handleCheckboxChange
(
checked
===
true
)
}
/>
</
span
>
<
p
className=
{
cn
(
complete
&&
"line-through opacity-80"
)
}
>
{
children
.
map
((
child
,
index
)
=>
(
...
...
web/src/components/MemoContent/index.tsx
View file @
493832ae
import
{
observer
}
from
"mobx-react-lite"
;
import
{
memo
,
useEffect
,
useRef
,
useState
}
from
"react"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
memoStore
}
from
"@/store/v2"
;
import
{
Node
,
NodeType
}
from
"@/types/proto/api/v1/markdown_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
isSuperUser
}
from
"@/utils/user"
;
import
Renderer
from
"./Renderer"
;
...
...
web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx
View file @
493832ae
import
{
isEqual
}
from
"lodash-es"
;
import
{
CheckCircleIcon
,
Code2Icon
,
HashIcon
,
LinkIcon
}
from
"lucide-react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Memo
,
MemoRelation_Type
,
Memo_Property
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
MemoRelationForceGraph
from
"../MemoRelationForceGraph"
;
...
...
web/src/components/MemoDetailSidebar/MemoDetailSidebarDrawer.tsx
View file @
493832ae
import
{
Drawer
}
from
"@mui/joy"
;
import
{
Button
}
from
"@usememos/mui"
;
import
{
GanttChartIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Sheet
,
SheetContent
,
SheetTrigger
}
from
"@/components/ui/sheet"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
MemoDetailSidebar
from
"./MemoDetailSidebar"
;
...
...
@@ -19,24 +19,17 @@ const MemoDetailSidebarDrawer = ({ memo, parentPage }: Props) => {
setOpen
(
false
);
},
[
location
.
pathname
]);
const
toggleDrawer
=
(
inOpen
:
boolean
)
=>
(
event
:
React
.
KeyboardEvent
|
React
.
MouseEvent
)
=>
{
if
(
event
.
type
===
"keydown"
&&
((
event
as
React
.
KeyboardEvent
).
key
===
"Tab"
||
(
event
as
React
.
KeyboardEvent
).
key
===
"Shift"
))
{
return
;
}
setOpen
(
inOpen
);
};
return
(
<>
<
Button
variant=
"plain"
className=
"bg-transparent! px-2"
onClick=
{
toggleDrawer
(
true
)
}
>
<
GanttChartIcon
className=
"w-5 h-auto dark:text-gray-400"
/
>
</
Button
>
<
Drawer
anchor=
"right"
size=
"sm"
open=
{
open
}
onClose=
{
toggleDrawer
(
false
)
}
>
<
div
className=
"w-full h-full px-4 bg-zinc-100 dark:bg-zinc-900"
>
<
MemoDetailSidebar
className=
"py-4"
memo=
{
memo
}
parentPage=
{
parentPage
}
/
>
<
/
div
>
</
Drawer
>
</>
<
Sheet
open=
{
open
}
onOpenChange=
{
setOpen
}
>
<
SheetTrigger
asChild
>
<
Button
variant=
"ghost"
className=
"bg-transparent! px-2"
>
<
GanttChartIcon
className=
"w-5 h-auto dark:text-gray-400"
/
>
</
Button
>
</
SheetTrigger
>
<
SheetContent
side=
"right"
className=
"w-full sm:w-80 px-4 bg-zinc-100 dark:bg-zinc-900"
>
<
MemoDetailSidebar
className=
"py-4"
memo=
{
memo
}
parentPage=
{
parentPage
}
/
>
</
SheetContent
>
</
Sheet
>
);
};
...
...
web/src/components/MemoDisplaySettingMenu.tsx
View file @
493832ae
import
{
Option
,
Select
}
from
"@mui/joy"
;
import
{
Settings2Icon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
viewStore
}
from
"@/store/v2"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"./ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"./ui/
p
opover"
;
interface
Props
{
className
?:
string
;
...
...
@@ -26,29 +26,39 @@ const MemoDisplaySettingMenu = observer(({ className }: Props) => {
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-sm shrink-0 mr-3 dark:text-zinc-400"
>
{
t
(
"memo.direction"
)
}
</
span
>
<
Select
value=
{
viewStore
.
state
.
orderByTimeAsc
}
on
Change=
{
(
_
,
value
)
=>
value=
{
viewStore
.
state
.
orderByTimeAsc
.
toString
()
}
on
ValueChange=
{
(
value
)
=>
viewStore
.
state
.
setPartial
({
orderByTimeAsc
:
Boolean
(
value
)
,
orderByTimeAsc
:
value
===
"true"
,
})
}
>
<
Option
value=
{
false
}
>
{
t
(
"memo.direction-desc"
)
}
</
Option
>
<
Option
value=
{
true
}
>
{
t
(
"memo.direction-asc"
)
}
</
Option
>
<
SelectTrigger
className=
"w-32"
>
<
SelectValue
/>
</
SelectTrigger
>
<
SelectContent
>
<
SelectItem
value=
"false"
>
{
t
(
"memo.direction-desc"
)
}
</
SelectItem
>
<
SelectItem
value=
"true"
>
{
t
(
"memo.direction-asc"
)
}
</
SelectItem
>
</
SelectContent
>
</
Select
>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-sm shrink-0 mr-3 dark:text-zinc-400"
>
{
t
(
"common.layout"
)
}
</
span
>
<
Select
value=
{
viewStore
.
state
.
layout
}
on
Change=
{
(
_
,
value
)
=>
on
ValueChange=
{
(
value
)
=>
viewStore
.
state
.
setPartial
({
layout
:
value
as
"LIST"
|
"MASONRY"
,
})
}
>
<
Option
value=
{
"LIST"
}
>
{
t
(
"memo.list"
)
}
</
Option
>
<
Option
value=
{
"MASONRY"
}
>
{
t
(
"memo.masonry"
)
}
</
Option
>
<
SelectTrigger
className=
"w-32"
>
<
SelectValue
/>
</
SelectTrigger
>
<
SelectContent
>
<
SelectItem
value=
"LIST"
>
{
t
(
"memo.list"
)
}
</
SelectItem
>
<
SelectItem
value=
"MASONRY"
>
{
t
(
"memo.masonry"
)
}
</
SelectItem
>
</
SelectContent
>
</
Select
>
</
div
>
</
div
>
...
...
web/src/components/MemoEditor/ActionButton/AddMemoRelationPopover.tsx
View file @
493832ae
import
{
Autocomplete
,
AutocompleteOption
,
Chip
}
from
"@mui/joy"
;
import
{
Button
,
Checkbox
}
from
"@usememos/mui"
;
import
{
uniqBy
}
from
"lodash-es"
;
import
{
LinkIcon
}
from
"lucide-react"
;
import
{
LinkIcon
,
X
}
from
"lucide-react"
;
import
React
,
{
useContext
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
useDebounce
from
"react-use/lib/useDebounce"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/Popover"
;
import
{
Badge
}
from
"@/components/ui/badge"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Checkbox
}
from
"@/components/ui/checkbox"
;
import
{
Command
,
CommandInput
,
CommandItem
,
CommandList
,
CommandEmpty
}
from
"@/components/ui/command"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/popover"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
{
DEFAULT_LIST_MEMOS_PAGE_SIZE
}
from
"@/helpers/consts"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
...
...
@@ -129,52 +131,69 @@ const AddMemoRelationPopover = (props: Props) => {
return
(
<
Popover
open=
{
popoverOpen
}
onOpenChange=
{
setPopoverOpen
}
>
<
PopoverTrigger
className=
"relative"
>
<
Button
className=
"flex items-center justify-center p-0"
variant=
"plain"
asChild
>
<
LinkIcon
className=
"w-5 h-5 mx-auto p-0"
/>
<
PopoverTrigger
asChild
>
<
Button
variant=
"ghost"
>
<
LinkIcon
/>
</
Button
>
</
PopoverTrigger
>
<
PopoverContent
align=
"center"
>
<
div
className=
"w-[16rem] p-1 flex flex-col justify-start items-start"
>
<
Autocomplete
className=
"w-full"
size=
"md"
clearOnBlur
disableClearable
placeholder=
{
t
(
"reference.search-placeholder"
)
}
noOptionsText=
{
t
(
"reference.no-memos-found"
)
}
options=
{
filteredMemos
}
loading=
{
isFetching
}
inputValue=
{
searchText
}
value=
{
selectedMemos
}
multiple
onInputChange=
{
(
_
,
value
)
=>
setSearchText
(
value
.
trimStart
())
}
getOptionKey=
{
(
memo
)
=>
memo
.
name
}
getOptionLabel=
{
(
memo
)
=>
memo
.
content
}
isOptionEqualToValue=
{
(
memo
,
value
)
=>
memo
.
name
===
value
.
name
}
renderOption=
{
(
props
,
memo
)
=>
(
<
AutocompleteOption
{
...
props
}
key=
{
memo
.
name
}
>
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
p
className=
"text-xs text-gray-400 select-none"
>
{
memo
.
displayTime
?.
toLocaleString
()
}
</
p
>
<
p
className=
"mt-0.5 text-sm leading-5 line-clamp-2"
>
{
searchText
?
getHighlightedContent
(
memo
.
content
)
:
memo
.
snippet
}
</
p
>
</
div
>
</
AutocompleteOption
>
)
}
renderTags=
{
(
memos
)
=>
memos
.
map
((
memo
)
=>
(
<
Chip
key=
{
memo
.
name
}
className=
"max-w-full! rounded!"
variant=
"outlined"
color=
"neutral"
>
{
/* Selected memos display */
}
{
selectedMemos
.
length
>
0
&&
(
<
div
className=
"w-full mb-2 flex flex-wrap gap-1"
>
{
selectedMemos
.
map
((
memo
)
=>
(
<
Badge
key=
{
memo
.
name
}
variant=
"outline"
className=
"max-w-full flex items-center gap-1 p-2"
>
<
div
className=
"flex-1 min-w-0"
>
<
p
className=
"text-xs text-gray-400 select-none"
>
{
memo
.
displayTime
?.
toLocaleString
()
}
</
p
>
<
span
className=
"text-sm leading-5 truncate block"
>
{
memo
.
content
}
</
span
>
</
div
>
<
X
className=
"w-3 h-3 cursor-pointer hover:text-red-500 flex-shrink-0"
onClick=
{
()
=>
setSelectedMemos
((
memos
)
=>
memos
.
filter
((
m
)
=>
m
.
name
!==
memo
.
name
))
}
/>
</
Badge
>
))
}
</
div
>
)
}
{
/* Search and selection interface */
}
<
Command
className=
"w-full"
>
<
CommandInput
placeholder=
{
t
(
"reference.search-placeholder"
)
}
value=
{
searchText
}
onValueChange=
{
setSearchText
}
className=
"h-9"
/>
<
CommandList
className=
"max-h-[200px]"
>
<
CommandEmpty
>
{
isFetching
?
"Loading..."
:
t
(
"reference.no-memos-found"
)
}
</
CommandEmpty
>
{
filteredMemos
.
map
((
memo
)
=>
(
<
CommandItem
key=
{
memo
.
name
}
value=
{
memo
.
name
}
onSelect=
{
()
=>
{
setSelectedMemos
((
prev
)
=>
[...
prev
,
memo
]);
}
}
className=
"cursor-pointer"
>
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
p
className=
"text-xs text-gray-400 select-none"
>
{
memo
.
displayTime
?.
toLocaleString
()
}
</
p
>
<
span
className=
"w-full text-sm leading-5 truncate"
>
{
memo
.
content
}
</
span
>
<
p
className=
"mt-0.5 text-sm leading-5 line-clamp-2"
>
{
searchText
?
getHighlightedContent
(
memo
.
content
)
:
memo
.
snippet
}
</
p
>
</
div
>
</
C
hip
>
))
}
onChange=
{
(
_
,
value
)
=>
setSelectedMemos
(
value
)
}
/>
</
C
ommandItem
>
))
}
</
CommandList
>
</
Command
>
<
div
className=
"mt-2 w-full flex flex-row justify-end items-center gap-2"
>
<
Checkbox
size=
"sm"
label=
{
"Embed"
}
checked=
{
embedded
}
onChange=
{
(
e
)
=>
setEmbedded
(
e
.
target
.
checked
)
}
/>
<
Button
color=
"primary"
onClick=
{
addMemoRelations
}
disabled=
{
selectedMemos
.
length
===
0
}
>
<
div
className=
"flex items-center space-x-2"
>
<
Checkbox
id=
"embed-checkbox"
checked=
{
embedded
}
onCheckedChange=
{
(
checked
)
=>
setEmbedded
(
checked
===
true
)
}
/>
<
label
htmlFor=
"embed-checkbox"
className=
"text-sm"
>
Embed
</
label
>
</
div
>
<
Button
onClick=
{
addMemoRelations
}
disabled=
{
selectedMemos
.
length
===
0
}
>
{
t
(
"common.add"
)
}
</
Button
>
</
div
>
...
...
web/src/components/MemoEditor/ActionButton/LocationSelector.tsx
View file @
493832ae
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
LatLng
}
from
"leaflet"
;
import
{
MapPinIcon
,
XIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
LeafletMap
from
"@/components/LeafletMap"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/Popover"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/popover"
;
import
{
Location
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
...
...
@@ -86,6 +87,7 @@ const LocationSelector = (props: Props) => {
};
const
removeLocation
=
(
e
:
React
.
MouseEvent
)
=>
{
console
.
log
(
"here"
);
e
.
preventDefault
();
e
.
stopPropagation
();
props
.
onChange
(
undefined
);
...
...
@@ -94,35 +96,39 @@ const LocationSelector = (props: Props) => {
return
(
<
Popover
open=
{
popoverOpen
}
onOpenChange=
{
setPopoverOpen
}
>
<
PopoverTrigger
asChild
>
<
Button
className=
"flex items-center justify-center p-0"
size=
"sm"
variant=
"plain"
>
<
MapPinIcon
className=
"w-5 h-5 mx-auto shrink-0"
/>
{
props
.
location
&&
(
<>
<
span
className=
"ml-0.5 text-sm text-ellipsis whitespace-nowrap overflow-hidden max-w-32"
>
{
props
.
location
.
placeholder
}
</
span
>
<
XIcon
className=
"w-5 h-5 mx-auto shrink-0 hidden group-hover:block opacity-60 hover:opacity-80"
onClick=
{
removeLocation
}
/>
</>
)
}
<
Button
variant=
"ghost"
asChild
>
<
div
>
<
MapPinIcon
className=
"w-5 h-5 mx-auto shrink-0"
/>
{
props
.
location
&&
(
<>
<
span
className=
"ml-0.5 text-sm text-ellipsis whitespace-nowrap overflow-hidden max-w-32"
>
{
props
.
location
.
placeholder
}
</
span
>
<
XIcon
className=
"w-5 h-5 mx-auto shrink-0 opacity-60 hover:opacity-80"
onClick=
{
removeLocation
}
/>
</>
)
}
</
div
>
</
Button
>
</
PopoverTrigger
>
<
PopoverContent
align=
"center"
>
<
div
className=
"min-w-80 sm:w-lg p-1 flex flex-col justify-start items-start"
>
<
LeafletMap
key=
{
JSON
.
stringify
(
state
.
initilized
)
}
latlng=
{
state
.
position
}
onChange=
{
onPositionChanged
}
/>
<
div
className=
"mt-2 w-full flex flex-row justify-between items-center gap-2"
>
<
div
className=
"flex flex-row items-center justify-start gap-2"
>
<
Input
placeholder=
"Choose a position first."
value=
{
state
.
placeholder
}
size=
"sm"
startDecorator=
{
state
.
position
&&
(
<
div
className=
"text-xs leading-6 opacity-60"
>
[
{
state
.
position
.
lat
.
toFixed
(
2
)
}
,
{
state
.
position
.
lng
.
toFixed
(
2
)
}
]
</
div
>
)
}
disabled=
{
!
state
.
position
}
onChange=
{
(
e
)
=>
setState
((
state
)
=>
({
...
state
,
placeholder
:
e
.
target
.
value
}))
}
/
>
<
div
className=
"flex flex-row items-center justify-start gap-2
w-full
"
>
<
div
className=
"relative flex-1"
>
{
state
.
position
&&
(
<
div
className=
"absolute left-2 top-1/2 -translate-y-1/2 text-xs leading-6 opacity-60 z-10"
>
[
{
state
.
position
.
lat
.
toFixed
(
2
)
}
,
{
state
.
position
.
lng
.
toFixed
(
2
)
}
]
</
div
>
)
}
<
Input
placeholder=
"Choose a position first."
value=
{
state
.
placeholder
}
disabled=
{
!
state
.
position
}
className=
{
state
.
position
?
"pl-24"
:
""
}
onChange=
{
(
e
)
=>
setState
((
state
)
=>
({
...
state
,
placeholder
:
e
.
target
.
value
}))
}
/>
</
div
>
</
div
>
<
Button
className=
"shrink-0"
...
...
web/src/components/MemoEditor/ActionButton/MarkdownMenu.tsx
View file @
493832ae
import
{
Link
}
from
"@mui/joy"
;
import
{
Button
}
from
"@usememos/mui"
;
import
{
CheckSquareIcon
,
Code2Icon
,
SquareSlashIcon
}
from
"lucide-react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../../ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../../ui/
p
opover"
;
import
{
EditorRefActions
}
from
"../Editor"
;
interface
Props
{
...
...
@@ -10,9 +9,8 @@ interface Props {
}
const
MarkdownMenu
=
(
props
:
Props
)
=>
{
const
t
=
useTranslate
();
const
{
editorRef
}
=
props
;
const
t
=
useTranslate
();
const
handleCodeBlockClick
=
()
=>
{
if
(
!
editorRef
.
current
)
{
...
...
@@ -64,8 +62,8 @@ const MarkdownMenu = (props: Props) => {
return
(
<
Popover
>
<
PopoverTrigger
asChild
>
<
Button
variant=
"
plain"
className=
"p-0
"
>
<
SquareSlashIcon
className=
"w-5 h-5"
/>
<
Button
variant=
"
ghost
"
>
<
SquareSlashIcon
/>
</
Button
>
</
PopoverTrigger
>
<
PopoverContent
align=
"start"
className=
"text-sm p-1"
>
...
...
@@ -85,9 +83,14 @@ const MarkdownMenu = (props: Props) => {
<
span
>
{
t
(
"markdown.checkbox"
)
}
</
span
>
</
button
>
<
div
className=
"pl-2"
>
<
Link
fontSize=
{
12
}
href=
"https://www.usememos.com/docs/getting-started/content-syntax"
target=
"_blank"
>
<
a
className=
"text-xs text-blue-600 hover:underline"
href=
"https://www.usememos.com/docs/getting-started/content-syntax"
target=
"_blank"
rel=
"noopener noreferrer"
>
{
t
(
"markdown.content-syntax"
)
}
</
Link
>
</
a
>
</
div
>
</
div
>
</
PopoverContent
>
...
...
web/src/components/MemoEditor/ActionButton/TagSelector.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
{
HashIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
OverflowTip
from
"@/components/kit/OverflowTip"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
userStore
}
from
"@/store/v2"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../../ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../../ui/
p
opover"
;
import
{
EditorRefActions
}
from
"../Editor"
;
interface
Props
{
...
...
@@ -35,8 +35,8 @@ const TagSelector = observer((props: Props) => {
return
(
<
Popover
>
<
PopoverTrigger
asChild
>
<
Button
variant=
"
plain"
className=
"p-0
"
>
<
HashIcon
className=
"w-5 h-5"
/>
<
Button
variant=
"
ghost
"
>
<
HashIcon
/>
</
Button
>
</
PopoverTrigger
>
<
PopoverContent
align=
"start"
sideOffset=
{
2
}
>
...
...
web/src/components/MemoEditor/ActionButton/UploadAttachmentButton.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
{
LoaderIcon
,
PaperclipIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useContext
,
useRef
,
useState
}
from
"react"
;
import
toast
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
attachmentStore
}
from
"@/store/v2"
;
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
{
MemoEditorContext
}
from
"../types"
;
...
...
@@ -73,7 +73,7 @@ const UploadAttachmentButton = observer((props: Props) => {
const
isUploading
=
state
.
uploadingFlag
||
props
.
isUploading
;
return
(
<
Button
className=
"relative
p-0"
variant=
"plain
"
disabled=
{
isUploading
}
>
<
Button
className=
"relative
"
variant=
"ghost
"
disabled=
{
isUploading
}
>
{
isUploading
?
<
LoaderIcon
className=
"w-5 h-5 animate-spin"
/>
:
<
PaperclipIcon
className=
"w-5 h-5"
/>
}
<
input
className=
"absolute inset-0 w-full h-full opacity-0 cursor-pointer"
...
...
web/src/components/MemoEditor/ActionButton/VisibilitySelector.tsx
View file @
493832ae
import
{
ChevronDownIcon
}
from
"lucide-react"
;
import
{
useState
}
from
"react"
;
import
VisibilityIcon
from
"@/components/VisibilityIcon"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/Popover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"@/components/ui/popover"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Visibility
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
interface
Props
{
...
...
web/src/components/MemoEditor/Editor/TagSuggestions.tsx
View file @
493832ae
...
...
@@ -3,8 +3,8 @@ import { observer } from "mobx-react-lite";
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
getCaretCoordinates
from
"textarea-caret"
;
import
OverflowTip
from
"@/components/kit/OverflowTip"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
userStore
}
from
"@/store/v2"
;
import
{
cn
}
from
"@/utils"
;
import
{
EditorRefActions
}
from
"."
;
type
Props
=
{
...
...
web/src/components/MemoEditor/Editor/index.tsx
View file @
493832ae
import
{
last
}
from
"lodash-es"
;
import
{
forwardRef
,
ReactNode
,
useCallback
,
useEffect
,
useImperativeHandle
,
useRef
,
useState
}
from
"react"
;
import
{
markdownServiceClient
}
from
"@/grpcweb"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Node
,
NodeType
,
OrderedListItemNode
,
TaskListItemNode
,
UnorderedListItemNode
}
from
"@/types/proto/api/v1/markdown_service"
;
import
{
cn
}
from
"@/utils"
;
import
TagSuggestions
from
"./TagSuggestions"
;
export
interface
EditorRefActions
{
...
...
web/src/components/MemoEditor/index.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
copy
from
"copy-to-clipboard"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
LoaderIcon
,
SendIcon
}
from
"lucide-react"
;
...
...
@@ -7,17 +6,18 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
import
{
toast
}
from
"react-hot-toast"
;
import
{
useTranslation
}
from
"react-i18next"
;
import
useLocalStorage
from
"react-use/lib/useLocalStorage"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
{
TAB_SPACE_WIDTH
}
from
"@/helpers/consts"
;
import
{
isValidUrl
}
from
"@/helpers/utils"
;
import
useAsyncEffect
from
"@/hooks/useAsyncEffect"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
extractMemoIdFromName
}
from
"@/store/common"
;
import
{
memoStore
,
attachmentStore
,
userStore
,
workspaceStore
}
from
"@/store/v2"
;
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
{
Location
,
Memo
,
MemoRelation
,
MemoRelation_Type
,
Visibility
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
UserSetting
}
from
"@/types/proto/api/v1/user_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
convertVisibilityFromString
}
from
"@/utils/memo"
;
import
DateTimeInput
from
"../DateTimeInput"
;
...
...
@@ -503,7 +503,7 @@ const MemoEditor = observer((props: Props) => {
<
AttachmentListView
attachmentList=
{
state
.
attachmentList
}
setAttachmentList=
{
handleSetAttachmentList
}
/>
<
RelationListView
relationList=
{
referenceRelations
}
setRelationList=
{
handleSetRelationList
}
/>
<
div
className=
"relative w-full flex flex-row justify-between items-center py-1"
onFocus=
{
(
e
)
=>
e
.
stopPropagation
()
}
>
<
div
className=
"flex flex-row justify-start items-center opacity-80 dark:opacity-60 space-x-2"
>
<
div
className=
"flex flex-row justify-start items-center opacity-80 dark:opacity-60
-
space-x-2"
>
<
TagSelector
editorRef=
{
editorRef
}
/>
<
MarkdownMenu
editorRef=
{
editorRef
}
/>
<
UploadAttachmentButton
isUploading=
{
state
.
isUploadingAttachment
}
/>
...
...
@@ -520,7 +520,7 @@ const MemoEditor = observer((props: Props) => {
</
div
>
<
div
className=
"shrink-0 -mr-1 flex flex-row justify-end items-center"
>
{
props
.
onCancel
&&
(
<
Button
variant=
"
plain
"
className=
"opacity-60"
disabled=
{
state
.
isRequesting
}
onClick=
{
handleCancelBtnClick
}
>
<
Button
variant=
"
ghost
"
className=
"opacity-60"
disabled=
{
state
.
isRequesting
}
onClick=
{
handleCancelBtnClick
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
)
}
...
...
web/src/components/MemoLocationView.tsx
View file @
493832ae
...
...
@@ -3,7 +3,7 @@ import { MapPinIcon } from "lucide-react";
import
{
useState
}
from
"react"
;
import
{
Location
}
from
"@/types/proto/api/v1/memo_service"
;
import
LeafletMap
from
"./LeafletMap"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"./ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"./ui/
p
opover"
;
interface
Props
{
location
:
Location
;
...
...
web/src/components/MemoRelationForceGraph/MemoRelationForceGraph.tsx
View file @
493832ae
import
{
useColorScheme
}
from
"@mui/joy"
;
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
ForceGraph2D
,
{
ForceGraphMethods
,
LinkObject
,
NodeObject
}
from
"react-force-graph-2d"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
extractMemoIdFromName
}
from
"@/store/common"
;
import
{
Memo
,
MemoRelation_Type
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
LinkType
,
NodeType
}
from
"./types"
;
import
{
convertMemoRelationsToGraphData
}
from
"./utils"
;
...
...
@@ -19,11 +18,30 @@ const DEFAULT_NODE_COLOR = "#a1a1aa";
const
MemoRelationForceGraph
=
({
className
,
memo
,
parentPage
}:
Props
)
=>
{
const
navigateTo
=
useNavigateTo
();
const
{
mode
}
=
useColorScheme
(
);
const
[
mode
,
setMode
]
=
useState
<
"light"
|
"dark"
>
(
"light"
);
const
containerRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
graphRef
=
useRef
<
ForceGraphMethods
<
NodeObject
<
NodeType
>
,
LinkObject
<
NodeType
,
LinkType
>>
|
undefined
>
(
undefined
);
const
[
graphSize
,
setGraphSize
]
=
useState
({
width
:
0
,
height
:
0
});
// Simple dark mode detection
useEffect
(()
=>
{
const
updateMode
=
()
=>
{
const
isDark
=
document
.
documentElement
.
classList
.
contains
(
"dark"
);
setMode
(
isDark
?
"dark"
:
"light"
);
};
updateMode
();
// Watch for changes to the dark class
const
observer
=
new
MutationObserver
(
updateMode
);
observer
.
observe
(
document
.
documentElement
,
{
attributes
:
true
,
attributeFilter
:
[
"class"
],
});
return
()
=>
observer
.
disconnect
();
},
[]);
useEffect
(()
=>
{
if
(
!
containerRef
.
current
)
return
;
setGraphSize
(
containerRef
.
current
.
getBoundingClientRect
());
...
...
web/src/components/MemoRelationListView.tsx
View file @
493832ae
import
{
LinkIcon
,
MilestoneIcon
}
from
"lucide-react"
;
import
{
memo
,
useState
}
from
"react"
;
import
{
Link
}
from
"react-router-dom"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
extractMemoIdFromName
}
from
"@/store/common"
;
import
{
Memo
,
MemoRelation
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
interface
Props
{
...
...
web/src/components/MemoView.tsx
View file @
493832ae
import
{
Tooltip
}
from
"@mui/joy"
;
import
{
BookmarkIcon
,
EyeOffIcon
,
MessageCircleMoreIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
memo
,
useCallback
,
useState
}
from
"react"
;
import
{
Link
,
useLocation
}
from
"react-router-dom"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
useAsyncEffect
from
"@/hooks/useAsyncEffect"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
memoStore
,
userStore
,
workspaceStore
}
from
"@/store/v2"
;
import
{
State
}
from
"@/types/proto/api/v1/common"
;
import
{
Memo
,
MemoRelation_Type
,
Visibility
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
{
convertVisibilityToString
}
from
"@/utils/memo"
;
import
{
isSuperUser
}
from
"@/utils/user"
;
...
...
@@ -170,10 +170,13 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
<
div
className=
"flex flex-row justify-end items-center select-none shrink-0 gap-2"
>
<
div
className=
"w-auto invisible group-hover:visible flex flex-row justify-between items-center gap-2"
>
{
props
.
showVisibility
&&
memo
.
visibility
!==
Visibility
.
PRIVATE
&&
(
<
Tooltip
title=
{
t
(
`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}`
as
any
)
}
placement=
"top"
>
<
span
className=
"flex justify-center items-center hover:opacity-70"
>
<
VisibilityIcon
visibility=
{
memo
.
visibility
}
/>
</
span
>
<
Tooltip
>
<
TooltipTrigger
>
<
span
className=
"flex justify-center items-center hover:opacity-70"
>
<
VisibilityIcon
visibility=
{
memo
.
visibility
}
/>
</
span
>
</
TooltipTrigger
>
<
TooltipContent
>
{
t
(
`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}`
as
any
)
}
</
TooltipContent
>
</
Tooltip
>
)
}
{
currentUser
&&
!
isArchived
&&
<
ReactionSelector
className=
"border-none w-auto h-auto"
memo=
{
memo
}
/>
}
...
...
@@ -195,11 +198,18 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
</
Link
>
)
}
{
props
.
showPinned
&&
memo
.
pinned
&&
(
<
Tooltip
title=
{
t
(
"common.unpin"
)
}
placement=
"top"
>
<
span
className=
"cursor-pointer"
>
<
BookmarkIcon
className=
"w-4 h-auto text-amber-500"
onClick=
{
onPinIconClick
}
/>
</
span
>
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
span
className=
"cursor-pointer"
>
<
BookmarkIcon
className=
"w-4 h-auto text-amber-500"
onClick=
{
onPinIconClick
}
/>
</
span
>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
t
(
"common.unpin"
)
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
)
}
{
nsfw
&&
showNSFWContent
&&
(
<
span
className=
"cursor-pointer"
>
...
...
web/src/components/MobileHeader.tsx
View file @
493832ae
import
useWindowScroll
from
"react-use/lib/useWindowScroll"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
{
cn
}
from
"@/utils"
;
import
{
cn
}
from
"@/
lib/
utils"
;
import
NavigationDrawer
from
"./NavigationDrawer"
;
interface
Props
{
...
...
web/src/components/Navigation.tsx
View file @
493832ae
import
{
Tooltip
}
from
"@mui/joy"
;
import
{
EarthIcon
,
LibraryIcon
,
PaperclipIcon
,
UserCircleIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useEffect
}
from
"react"
;
import
{
NavLink
}
from
"react-router-dom"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Routes
}
from
"@/router"
;
import
{
userStore
}
from
"@/store/v2"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
BrandBanner
from
"./BrandBanner"
;
import
UserBanner
from
"./UserBanner"
;
...
...
@@ -89,9 +89,16 @@ const Navigation = observer((props: Props) => {
viewTransition
>
{
props
.
collapsed
?
(
<
Tooltip
title=
{
navLink
.
title
}
placement=
"right"
arrow
>
<
div
>
{
navLink
.
icon
}
</
div
>
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
div
>
{
navLink
.
icon
}
</
div
>
</
TooltipTrigger
>
<
TooltipContent
side=
"right"
>
<
p
>
{
navLink
.
title
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
)
:
(
navLink
.
icon
)
}
...
...
web/src/components/NavigationDrawer.tsx
View file @
493832ae
import
{
Drawer
}
from
"@mui/joy"
;
import
{
Button
}
from
"@usememos/mui"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useLocation
}
from
"react-router-dom"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Sheet
,
SheetContent
,
SheetTrigger
}
from
"@/components/ui/sheet"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
Navigation
from
"./Navigation"
;
import
UserAvatar
from
"./UserAvatar"
;
...
...
@@ -18,28 +18,20 @@ const NavigationDrawer = observer(() => {
setOpen
(
false
);
},
[
location
.
pathname
]);
const
toggleDrawer
=
(
inOpen
:
boolean
)
=>
(
event
:
React
.
KeyboardEvent
|
React
.
MouseEvent
)
=>
{
if
(
event
.
type
===
"keydown"
&&
((
event
as
React
.
KeyboardEvent
).
key
===
"Tab"
||
(
event
as
React
.
KeyboardEvent
).
key
===
"Shift"
))
{
return
;
}
setOpen
(
inOpen
);
};
return
(
<>
<
Button
variant=
"plain"
className=
"px-2"
onClick=
{
toggleDrawer
(
true
)
}
>
<
UserAvatar
className=
"shrink-0 w-6 h-6 rounded-md"
avatarUrl=
{
avatarUrl
}
/
>
<
span
className=
"font-bold text-lg leading-10 ml-2 text-ellipsis shrink-0 cursor-pointer overflow-hidden text-gray-700 dark:text-gray-300"
>
{
title
}
</
span
>
</
Butto
n
>
<
Drawer
anchor=
"left"
size=
"sm"
open=
{
open
}
onClose=
{
toggleDrawer
(
false
)
}
>
<
div
className=
"w-full h-full overflow-auto px-2 bg-zinc-100 dark:bg-zinc-900"
>
<
Navigation
/
>
<
/
div
>
</
Drawer
>
</>
<
Sheet
open=
{
open
}
onOpenChange=
{
setOpen
}
>
<
SheetTrigger
asChild
>
<
Button
variant=
"ghost"
className=
"px-2"
>
<
UserAvatar
className=
"shrink-0 w-6 h-6 rounded-md"
avatarUrl=
{
avatarUrl
}
/
>
<
span
className=
"font-bold text-lg leading-10 ml-2 text-ellipsis shrink-0 cursor-pointer overflow-hidden text-gray-700 dark:text-gray-300"
>
{
title
}
</
spa
n
>
</
Button
>
</
SheetTrigger
>
<
SheetContent
side=
"left"
className=
"w-full sm:w-80 overflow-auto px-2 bg-zinc-100 dark:bg-zinc-900"
>
<
Navigation
/
>
</
SheetContent
>
</
Sheet
>
);
});
...
...
web/src/components/PagedMemoList/PagedMemoList.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
{
ArrowUpIcon
,
LoaderIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useCallback
,
useEffect
,
useRef
,
useState
}
from
"react"
;
import
{
matchPath
}
from
"react-router-dom"
;
import
PullToRefresh
from
"react-simple-pull-to-refresh"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
DEFAULT_LIST_MEMOS_PAGE_SIZE
}
from
"@/helpers/consts"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
{
Routes
}
from
"@/router"
;
...
...
@@ -220,7 +220,7 @@ const BackToTop = () => {
}
return
(
<
Button
variant=
"
plain
"
onClick=
{
scrollToTop
}
>
<
Button
variant=
"
ghost
"
onClick=
{
scrollToTop
}
>
{
t
(
"router.back-to-top"
)
}
<
ArrowUpIcon
className=
"ml-1 w-4 h-auto"
/>
</
Button
>
...
...
web/src/components/PasswordSignInForm.tsx
View file @
493832ae
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
LoaderIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
ClientError
}
from
"nice-grpc-web"
;
import
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
authServiceClient
}
from
"@/grpcweb"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
...
...
@@ -62,8 +63,7 @@ const PasswordSignInForm = observer(() => {
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
span
className=
"leading-8 text-gray-600"
>
{
t
(
"common.username"
)
}
</
span
>
<
Input
className=
"w-full bg-white dark:bg-black"
size=
"lg"
className=
"w-full bg-white dark:bg-black h-10"
type=
"text"
readOnly=
{
actionBtnLoadingState
.
isLoading
}
placeholder=
{
t
(
"common.username"
)
}
...
...
@@ -78,8 +78,7 @@ const PasswordSignInForm = observer(() => {
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
span
className=
"leading-8 text-gray-600"
>
{
t
(
"common.password"
)
}
</
span
>
<
Input
className=
"w-full bg-white dark:bg-black"
size=
"lg"
className=
"w-full bg-white dark:bg-black h-10"
type=
"password"
readOnly=
{
actionBtnLoadingState
.
isLoading
}
placeholder=
{
t
(
"common.password"
)
}
...
...
@@ -93,14 +92,7 @@ const PasswordSignInForm = observer(() => {
</
div
>
</
div
>
<
div
className=
"flex flex-row justify-end items-center w-full mt-6"
>
<
Button
type=
"submit"
color=
"primary"
size=
"lg"
fullWidth
disabled=
{
actionBtnLoadingState
.
isLoading
}
onClick=
{
handleSignInButtonClick
}
>
<
Button
type=
"submit"
className=
"w-full h-10"
disabled=
{
actionBtnLoadingState
.
isLoading
}
onClick=
{
handleSignInButtonClick
}
>
{
t
(
"common.sign-in"
)
}
{
actionBtnLoadingState
.
isLoading
&&
<
LoaderIcon
className=
"w-5 h-auto ml-2 animate-spin opacity-60"
/>
}
</
Button
>
...
...
web/src/components/PreviewImageDialog.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
generateDialog
}
from
"./Dialog"
;
const
MIN_SCALE
=
0.5
;
...
...
web/src/components/ReactionSelector.tsx
View file @
493832ae
...
...
@@ -4,10 +4,10 @@ import { useRef, useState } from "react";
import
useClickAway
from
"react-use/lib/useClickAway"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
memoStore
,
workspaceStore
}
from
"@/store/v2"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"./ui/Popover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"./ui/popover"
;
interface
Props
{
memo
:
Memo
;
...
...
web/src/components/ReactionView.tsx
View file @
493832ae
import
{
Tooltip
}
from
"@mui/joy"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
memoStore
}
from
"@/store/v2"
;
import
{
State
}
from
"@/types/proto/api/v1/common"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
User
}
from
"@/types/proto/api/v1/user_service"
;
import
{
cn
}
from
"@/utils"
;
interface
Props
{
memo
:
Memo
;
...
...
@@ -65,20 +65,27 @@ const ReactionView = observer((props: Props) => {
};
return
(
<
Tooltip
title=
{
stringifyUsers
(
users
,
reactionType
)
}
placement=
"top"
>
<
div
className=
{
cn
(
"h-7 border border-zinc-200 px-2 py-0.5 rounded-full flex flex-row justify-center items-center gap-1 dark:border-zinc-700"
,
"text-sm text-gray-600 dark:text-gray-400"
,
currentUser
&&
!
readonly
&&
"cursor-pointer"
,
hasReaction
&&
"bg-blue-100 border-blue-200 dark:bg-zinc-900"
,
)
}
onClick=
{
handleReactionClick
}
>
<
span
>
{
reactionType
}
</
span
>
<
span
className=
"opacity-60"
>
{
users
.
length
}
</
span
>
</
div
>
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
div
className=
{
cn
(
"h-7 border border-zinc-200 px-2 py-0.5 rounded-full flex flex-row justify-center items-center gap-1 dark:border-zinc-700"
,
"text-sm text-gray-600 dark:text-gray-400"
,
currentUser
&&
!
readonly
&&
"cursor-pointer"
,
hasReaction
&&
"bg-blue-100 border-blue-200 dark:bg-zinc-900"
,
)
}
onClick=
{
handleReactionClick
}
>
<
span
>
{
reactionType
}
</
span
>
<
span
className=
"opacity-60"
>
{
users
.
length
}
</
span
>
</
div
>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
stringifyUsers
(
users
,
reactionType
)
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
);
});
...
...
web/src/components/RenameTagDialog.tsx
View file @
493832ae
import
{
List
,
ListItem
}
from
"@mui/joy"
;
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
React
,
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
memoServiceClient
}
from
"@/grpcweb"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
...
...
@@ -50,7 +50,7 @@ const RenameTagDialog: React.FC<Props> = (props: Props) => {
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
t
(
"tag.rename-tag"
)
}
</
p
>
<
Button
variant=
"
plain
"
onClick=
{
()
=>
destroy
()
}
>
<
Button
variant=
"
ghost
"
onClick=
{
()
=>
destroy
()
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
...
...
@@ -64,17 +64,17 @@ const RenameTagDialog: React.FC<Props> = (props: Props) => {
<
span
className=
"w-20 text-sm whitespace-nowrap shrink-0 text-right"
>
{
t
(
"tag.new-name"
)
}
</
span
>
<
Input
className=
"w-full"
type=
"text"
placeholder=
"A new tag name"
value=
{
newName
}
onChange=
{
handleTagNameInputChange
}
/>
</
div
>
<
List
size=
"sm"
marker=
"disc
"
>
<
ListItem
>
<
ul
className=
"list-disc list-inside text-sm ml-4
"
>
<
li
>
<
p
className=
"leading-5"
>
{
t
(
"tag.rename-tip"
)
}
</
p
>
</
ListItem
>
</
List
>
</
li
>
</
ul
>
</
div
>
<
div
className=
"w-full flex flex-row justify-end items-center space-x-2"
>
<
Button
variant=
"
plain
"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
<
Button
variant=
"
ghost
"
disabled=
{
requestState
.
isLoading
}
onClick=
{
destroy
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
<
Button
disabled=
{
requestState
.
isLoading
}
onClick=
{
handleConfirm
}
>
{
t
(
"common.confirm"
)
}
</
Button
>
</
div
>
...
...
web/src/components/SearchBar.tsx
View file @
493832ae
import
{
SearchIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useState
}
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
memoFilterStore
}
from
"@/store/v2"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
MemoDisplaySettingMenu
from
"./MemoDisplaySettingMenu"
;
...
...
web/src/components/Settings/AccessTokenSection.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
copy
from
"copy-to-clipboard"
;
import
{
ClipboardIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
UserAccessToken
}
from
"@/types/proto/api/v1/user_service"
;
...
...
@@ -99,7 +99,7 @@ const AccessTokenSection = () => {
<
tr
key=
{
userAccessToken
.
accessToken
}
>
<
td
className=
"whitespace-nowrap px-3 py-2 text-sm text-gray-900 dark:text-gray-400 flex flex-row justify-start items-center gap-x-1"
>
<
span
className=
"font-mono"
>
{
getFormatedAccessToken
(
userAccessToken
.
accessToken
)
}
</
span
>
<
Button
variant=
"
plain
"
onClick=
{
()
=>
copyAccessToken
(
userAccessToken
.
accessToken
)
}
>
<
Button
variant=
"
ghost
"
onClick=
{
()
=>
copyAccessToken
(
userAccessToken
.
accessToken
)
}
>
<
ClipboardIcon
className=
"w-4 h-auto text-gray-400"
/>
</
Button
>
</
td
>
...
...
@@ -114,7 +114,7 @@ const AccessTokenSection = () => {
</
td
>
<
td
className=
"relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm"
>
<
Button
variant=
"
plain
"
variant=
"
ghost
"
onClick=
{
()
=>
{
handleDeleteAccessToken
(
userAccessToken
);
}
}
...
...
web/src/components/Settings/MemberSection.tsx
View file @
493832ae
import
{
Radio
,
RadioGroup
}
from
"@mui/joy"
;
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
sortBy
}
from
"lodash-es"
;
import
{
MoreVerticalIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
React
,
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
userStore
}
from
"@/store/v2"
;
...
...
@@ -12,7 +14,7 @@ import { State } from "@/types/proto/api/v1/common";
import
{
User
,
User_Role
}
from
"@/types/proto/api/v1/user_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
showCreateUserDialog
from
"../CreateUserDialog"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/
p
opover"
;
interface
LocalState
{
creatingUser
:
User
;
...
...
@@ -167,15 +169,23 @@ const MemberSection = observer(() => {
</
div
>
<
div
className=
"flex flex-col justify-start items-start gap-1"
>
<
span
>
{
t
(
"common.role"
)
}
</
span
>
<
RadioGroup
orientation=
"horizontal"
defaultValue=
{
User_Role
.
USER
}
onChange=
{
handleUserRoleInputChange
}
>
<
Radio
value=
{
User_Role
.
USER
}
label=
{
t
(
"setting.member-section.user"
)
}
/>
<
Radio
value=
{
User_Role
.
ADMIN
}
label=
{
t
(
"setting.member-section.admin"
)
}
/>
<
RadioGroup
defaultValue=
{
User_Role
.
USER
}
onValueChange=
{
(
value
)
=>
handleUserRoleInputChange
({
target
:
{
value
}
}
as
React
.
ChangeEvent
<
HTMLInputElement
>
)
}
className=
"flex flex-row gap-4"
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
User_Role
.
USER
}
id=
"user-role"
/>
<
Label
htmlFor=
"user-role"
>
{
t
(
"setting.member-section.user"
)
}
</
Label
>
</
div
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
User_Role
.
ADMIN
}
id=
"admin-role"
/>
<
Label
htmlFor=
"admin-role"
>
{
t
(
"setting.member-section.admin"
)
}
</
Label
>
</
div
>
</
RadioGroup
>
</
div
>
<
div
className=
"mt-2"
>
<
Button
color=
"primary"
onClick=
{
handleCreateUserBtnClick
}
>
{
t
(
"common.create"
)
}
</
Button
>
<
Button
onClick=
{
handleCreateUserBtnClick
}
>
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center mt-6"
>
...
...
web/src/components/Settings/MemoRelatedSettings.tsx
View file @
493832ae
import
{
Chip
,
ChipDelete
}
from
"@mui/joy"
;
import
{
Button
,
Input
,
Switch
}
from
"@usememos/mui"
;
import
{
isEqual
,
uniq
}
from
"lodash-es"
;
import
{
CheckIcon
}
from
"lucide-react"
;
import
{
CheckIcon
,
X
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Badge
}
from
"@/components/ui/badge"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
workspaceSettingNamePrefix
}
from
"@/store/common"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
{
WorkspaceSettingKey
}
from
"@/store/v2/workspace"
;
...
...
@@ -70,42 +72,42 @@ const MemoRelatedSettings = observer(() => {
<
span
>
{
t
(
"setting.system-section.disable-public-memos"
)
}
</
span
>
<
Switch
checked=
{
memoRelatedSetting
.
disallowPublicVisibility
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
disallowPublicVisibility
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowPublicVisibility
:
checked
})
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.system-section.display-with-updated-time"
)
}
</
span
>
<
Switch
checked=
{
memoRelatedSetting
.
displayWithUpdateTime
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
displayWithUpdateTime
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
displayWithUpdateTime
:
checked
})
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.memo-related-settings.enable-link-preview"
)
}
</
span
>
<
Switch
checked=
{
memoRelatedSetting
.
enableLinkPreview
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
enableLinkPreview
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
enableLinkPreview
:
checked
})
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.memo-related-settings.enable-memo-comments"
)
}
</
span
>
<
Switch
checked=
{
memoRelatedSetting
.
enableComment
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
enableComment
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
enableComment
:
checked
})
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.system-section.enable-double-click-to-edit"
)
}
</
span
>
<
Switch
checked=
{
memoRelatedSetting
.
enableDoubleClickEdit
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
enableDoubleClickEdit
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
enableDoubleClickEdit
:
checked
})
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.system-section.disable-markdown-shortcuts-in-editor"
)
}
</
span
>
<
Switch
checked=
{
memoRelatedSetting
.
disableMarkdownShortcuts
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
disableMarkdownShortcuts
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disableMarkdownShortcuts
:
checked
})
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
...
...
@@ -122,33 +124,27 @@ const MemoRelatedSettings = observer(() => {
<
div
className=
"mt-2 w-full flex flex-row flex-wrap gap-1"
>
{
memoRelatedSetting
.
reactions
.
map
((
reactionType
)
=>
{
return
(
<
Chip
className=
"h-8!"
key=
{
reactionType
}
variant=
"outlined"
size=
"lg"
endDecorator=
{
<
ChipDelete
onDelete=
{
()
=>
updatePartialSetting
({
reactions
:
memoRelatedSetting
.
reactions
.
filter
((
r
)
=>
r
!==
reactionType
)
})
}
/>
}
>
<
Badge
key=
{
reactionType
}
variant=
"outline"
className=
"h-8 flex items-center gap-1"
>
{
reactionType
}
</
Chip
>
<
X
className=
"w-3 h-3 cursor-pointer hover:text-red-500"
onClick=
{
()
=>
updatePartialSetting
({
reactions
:
memoRelatedSetting
.
reactions
.
filter
((
r
)
=>
r
!==
reactionType
)
})
}
/>
</
Badge
>
);
})
}
<
Input
className=
"w-32 rounded-full! pl-1!"
placeholder=
{
t
(
"common.input"
)
}
value=
{
editingReaction
}
onChange=
{
(
event
)
=>
setEditingReaction
(
event
.
target
.
value
.
trim
())
}
endDecorator=
{
<
CheckIcon
className=
"w-5 h-5 text-gray-500 dark:text-gray-400 cursor-pointer hover:text-teal-600"
onClick=
{
()
=>
upsertReaction
()
}
/>
}
/
>
<
div
className=
"flex items-center gap-1"
>
<
Input
className=
"w-32"
placeholder=
{
t
(
"common.input"
)
}
value=
{
editingReaction
}
onChange=
{
(
event
)
=>
setEditingReaction
(
event
.
target
.
value
.
trim
())
}
/>
<
CheckIcon
className=
"w-5 h-5 text-gray-500 dark:text-gray-400 cursor-pointer hover:text-teal-600"
onClick=
{
()
=>
upsertReaction
()
}
/>
</
div
>
</
div
>
</
div
>
<
div
className=
"w-full"
>
...
...
@@ -156,43 +152,37 @@ const MemoRelatedSettings = observer(() => {
<
span
>
{
t
(
"setting.memo-related-settings.enable-blur-nsfw-content"
)
}
</
span
>
<
Switch
checked=
{
memoRelatedSetting
.
enableBlurNsfwContent
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
enableBlurNsfwContent
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
enableBlurNsfwContent
:
checked
})
}
/>
</
div
>
<
div
className=
"mt-2 w-full flex flex-row flex-wrap gap-1"
>
{
memoRelatedSetting
.
nsfwTags
.
map
((
nsfwTag
)
=>
{
return
(
<
Chip
className=
"h-8!"
key=
{
nsfwTag
}
variant=
"outlined"
size=
"lg"
endDecorator=
{
<
ChipDelete
onDelete=
{
()
=>
updatePartialSetting
({
nsfwTags
:
memoRelatedSetting
.
nsfwTags
.
filter
((
r
)
=>
r
!==
nsfwTag
)
})
}
/>
}
>
<
Badge
key=
{
nsfwTag
}
variant=
"outline"
className=
"h-8 flex items-center gap-1"
>
{
nsfwTag
}
</
Chip
>
<
X
className=
"w-3 h-3 cursor-pointer hover:text-red-500"
onClick=
{
()
=>
updatePartialSetting
({
nsfwTags
:
memoRelatedSetting
.
nsfwTags
.
filter
((
r
)
=>
r
!==
nsfwTag
)
})
}
/>
</
Badge
>
);
})
}
<
Input
className=
"w-32 rounded-full! pl-1!"
placeholder=
{
t
(
"common.input"
)
}
value=
{
editingNsfwTag
}
onChange=
{
(
event
)
=>
setEditingNsfwTag
(
event
.
target
.
value
.
trim
())
}
endDecorator=
{
<
CheckIcon
className=
"w-5 h-5 text-gray-500 dark:text-gray-400 cursor-pointer hover:text-teal-600"
onClick=
{
()
=>
upsertNsfwTags
()
}
/>
}
/
>
<
div
className=
"flex items-center gap-1"
>
<
Input
className=
"w-32"
placeholder=
{
t
(
"common.input"
)
}
value=
{
editingNsfwTag
}
onChange=
{
(
event
)
=>
setEditingNsfwTag
(
event
.
target
.
value
.
trim
())
}
/>
<
CheckIcon
className=
"w-5 h-5 text-gray-500 dark:text-gray-400 cursor-pointer hover:text-teal-600"
onClick=
{
()
=>
upsertNsfwTags
()
}
/>
</
div
>
</
div
>
</
div
>
<
div
className=
"mt-2 w-full flex justify-end"
>
<
Button
color=
"primary"
disabled=
{
isEqual
(
memoRelatedSetting
,
originalSetting
)
}
onClick=
{
updateSetting
}
>
<
Button
disabled=
{
isEqual
(
memoRelatedSetting
,
originalSetting
)
}
onClick=
{
updateSetting
}
>
{
t
(
"common.save"
)
}
</
Button
>
</
div
>
...
...
web/src/components/Settings/MyAccountSection.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
{
MoreVerticalIcon
,
PenLineIcon
}
from
"lucide-react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
showChangeMemberPasswordDialog
from
"../ChangeMemberPasswordDialog"
;
import
showUpdateAccountDialog
from
"../UpdateAccountDialog"
;
import
UserAvatar
from
"../UserAvatar"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/
p
opover"
;
import
AccessTokenSection
from
"./AccessTokenSection"
;
import
UserSessionsSection
from
"./UserSessionsSection"
;
...
...
@@ -27,13 +27,13 @@ const MyAccountSection = () => {
</
div
>
</
div
>
<
div
className=
"w-full flex flex-row justify-start items-center mt-2 space-x-2"
>
<
Button
variant=
"outline
d
"
onClick=
{
showUpdateAccountDialog
}
>
<
Button
variant=
"outline"
onClick=
{
showUpdateAccountDialog
}
>
<
PenLineIcon
className=
"w-4 h-4 mx-auto mr-1"
/>
{
t
(
"common.edit"
)
}
</
Button
>
<
Popover
>
<
PopoverTrigger
asChild
>
<
Button
variant=
"outline
d
"
>
<
Button
variant=
"outline"
>
<
MoreVerticalIcon
className=
"w-4 h-4 mx-auto"
/>
</
Button
>
</
PopoverTrigger
>
...
...
web/src/components/Settings/PreferencesSection.tsx
View file @
493832ae
import
{
Divider
,
Option
,
Select
}
from
"@mui/joy"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
userStore
}
from
"@/store/v2"
;
import
{
Visibility
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
UserSetting
}
from
"@/types/proto/api/v1/user_service"
;
...
...
@@ -44,27 +45,26 @@ const PreferencesSection = observer(() => {
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"truncate"
>
{
t
(
"setting.preference-section.default-memo-visibility"
)
}
</
span
>
<
Select
className=
"min-w-fit!"
value=
{
setting
.
memoVisibility
}
startDecorator=
{
<
VisibilityIcon
visibility=
{
convertVisibilityFromString
(
setting
.
memoVisibility
)
}
/>
}
onChange=
{
(
_
,
visibility
)
=>
{
if
(
visibility
)
{
handleDefaultMemoVisibilityChanged
(
visibility
);
}
}
}
>
{
[
Visibility
.
PRIVATE
,
Visibility
.
PROTECTED
,
Visibility
.
PUBLIC
]
.
map
((
v
)
=>
convertVisibilityToString
(
v
))
.
map
((
item
)
=>
(
<
Option
key=
{
item
}
value=
{
item
}
className=
"whitespace-nowrap"
>
{
t
(
`memo.visibility.${item.toLowerCase() as Lowercase<typeof item>}`
)
}
</
Option
>
))
}
<
Select
value=
{
setting
.
memoVisibility
}
onValueChange=
{
handleDefaultMemoVisibilityChanged
}
>
<
SelectTrigger
className=
"min-w-fit"
>
<
div
className=
"flex items-center gap-2"
>
<
VisibilityIcon
visibility=
{
convertVisibilityFromString
(
setting
.
memoVisibility
)
}
/>
<
SelectValue
/>
</
div
>
</
SelectTrigger
>
<
SelectContent
>
{
[
Visibility
.
PRIVATE
,
Visibility
.
PROTECTED
,
Visibility
.
PUBLIC
]
.
map
((
v
)
=>
convertVisibilityToString
(
v
))
.
map
((
item
)
=>
(
<
SelectItem
key=
{
item
}
value=
{
item
}
className=
"whitespace-nowrap"
>
{
t
(
`memo.visibility.${item.toLowerCase() as Lowercase<typeof item>}`
)
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
</
div
>
<
Divider
className=
"my-3!
"
/>
<
Separator
className=
"my-3
"
/>
<
WebhookSection
/>
</
div
>
...
...
web/src/components/Settings/SSOSection.tsx
View file @
493832ae
import
{
Divider
,
List
,
ListItem
}
from
"@mui/joy"
;
import
{
Button
}
from
"@usememos/mui"
;
import
{
MoreVerticalIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Link
}
from
"react-router-dom"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
IdentityProvider
}
from
"@/types/proto/api/v1/idp_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
showCreateIdentityProviderDialog
from
"../CreateIdentityProviderDialog"
;
import
LearnMore
from
"../LearnMore"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"../ui/
p
opover"
;
const
SSOSection
=
()
=>
{
const
t
=
useTranslate
();
...
...
@@ -48,7 +48,7 @@ const SSOSection = () => {
{
t
(
"common.create"
)
}
</
Button
>
</
div
>
<
Divide
r
/>
<
Separato
r
/>
{
identityProviderList
.
map
((
identityProvider
)
=>
(
<
div
key=
{
identityProvider
.
name
}
...
...
@@ -95,8 +95,8 @@ const SSOSection = () => {
<
div
className=
"w-full mt-4"
>
<
p
className=
"text-sm"
>
{
t
(
"common.learn-more"
)
}
:
</
p
>
<
List
component=
"ul"
marker=
"disc"
size=
"sm
"
>
<
ListItem
>
<
ul
className=
"list-disc list-inside text-sm ml-4
"
>
<
li
>
<
Link
className=
"text-sm text-blue-600 hover:underline"
to=
"https://www.usememos.com/docs/advanced-settings/sso"
...
...
@@ -104,8 +104,8 @@ const SSOSection = () => {
>
{
t
(
"setting.sso-section.single-sign-on"
)
}
</
Link
>
</
ListItem
>
</
List
>
</
li
>
</
ul
>
</
div
>
</
div
>
);
...
...
web/src/components/Settings/StorageSection.tsx
View file @
493832ae
import
{
Divider
,
List
,
ListItem
,
Radio
,
RadioGroup
,
Tooltip
}
from
"@mui/joy"
;
import
{
Button
,
Input
,
Switch
}
from
"@usememos/mui"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
HelpCircleIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
React
,
{
useEffect
,
useMemo
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Link
}
from
"react-router-dom"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
{
RadioGroup
,
RadioGroupItem
}
from
"@/components/ui/radio-group"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
workspaceSettingNamePrefix
}
from
"@/store/common"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
{
WorkspaceSettingKey
}
from
"@/store/v2/workspace"
;
...
...
@@ -131,23 +136,38 @@ const StorageSection = observer(() => {
<
div
className=
"w-full flex flex-col gap-2 pt-2 pb-4"
>
<
div
className=
"font-medium text-gray-700 dark:text-gray-500"
>
{
t
(
"setting.storage-section.current-storage"
)
}
</
div
>
<
RadioGroup
orientation=
"horizontal"
className=
"w-full"
value=
{
workspaceStorageSetting
.
storageType
}
onChange=
{
(
event
)
=>
{
handleStorageTypeChanged
(
event
.
target
.
value
as
WorkspaceStorageSetting_StorageType
);
value=
{
workspaceStorageSetting
.
storageType
.
toString
()
}
onValueChange=
{
(
value
)
=>
{
handleStorageTypeChanged
(
parseInt
(
value
)
as
unknown
as
WorkspaceStorageSetting_StorageType
);
}
}
className=
"flex flex-row gap-4"
>
<
Radio
value=
{
WorkspaceStorageSetting_StorageType
.
DATABASE
}
label=
{
t
(
"setting.storage-section.type-database"
)
}
/>
<
Radio
value=
{
WorkspaceStorageSetting_StorageType
.
LOCAL
}
label=
{
t
(
"setting.storage-section.type-local"
)
}
/>
<
Radio
value=
{
WorkspaceStorageSetting_StorageType
.
S3
}
label=
{
"S3"
}
/>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
WorkspaceStorageSetting_StorageType
.
DATABASE
.
toString
()
}
id=
"database"
/>
<
Label
htmlFor=
"database"
>
{
t
(
"setting.storage-section.type-database"
)
}
</
Label
>
</
div
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
WorkspaceStorageSetting_StorageType
.
LOCAL
.
toString
()
}
id=
"local"
/>
<
Label
htmlFor=
"local"
>
{
t
(
"setting.storage-section.type-local"
)
}
</
Label
>
</
div
>
<
div
className=
"flex items-center space-x-2"
>
<
RadioGroupItem
value=
{
WorkspaceStorageSetting_StorageType
.
S3
.
toString
()
}
id=
"s3"
/>
<
Label
htmlFor=
"s3"
>
S3
</
Label
>
</
div
>
</
RadioGroup
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
div
className=
"flex flex-row items-center"
>
<
span
className=
"text-gray-700 dark:text-gray-500 mr-1"
>
{
t
(
"setting.system-section.max-upload-size"
)
}
</
span
>
<
Tooltip
title=
{
t
(
"setting.system-section.max-upload-size-hint"
)
}
placement=
"top"
>
<
HelpCircleIcon
className=
"w-4 h-auto"
/>
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
>
<
HelpCircleIcon
className=
"w-4 h-auto"
/>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
t
(
"setting.system-section.max-upload-size-hint"
)
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
</
div
>
<
Input
className=
"w-16 font-mono"
value=
{
workspaceStorageSetting
.
uploadSizeLimitMb
}
onChange=
{
handleMaxUploadSizeChanged
}
/>
</
div
>
...
...
@@ -189,20 +209,23 @@ const StorageSection = observer(() => {
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-gray-700 dark:text-gray-500 mr-1"
>
Use Path Style
</
span
>
<
Switch
checked=
{
workspaceStorageSetting
.
s3Config
?.
usePathStyle
}
onChange=
{
handleS3ConfigUsePathStyleChanged
}
/>
<
Switch
checked=
{
workspaceStorageSetting
.
s3Config
?.
usePathStyle
}
onCheckedChange=
{
(
checked
)
=>
handleS3ConfigUsePathStyleChanged
({
target
:
{
checked
}
}
as
any
)
}
/>
</
div
>
</>
)
}
<
div
>
<
Button
color=
"primary"
disabled=
{
!
allowSaveStorageSetting
}
onClick=
{
saveWorkspaceStorageSetting
}
>
<
Button
disabled=
{
!
allowSaveStorageSetting
}
onClick=
{
saveWorkspaceStorageSetting
}
>
{
t
(
"common.save"
)
}
</
Button
>
</
div
>
<
Divider
className=
"my-2!
"
/>
<
Separator
className=
"my-2
"
/>
<
div
className=
"w-full mt-4"
>
<
p
className=
"text-sm"
>
{
t
(
"common.learn-more"
)
}
:
</
p
>
<
List
component=
"ul"
marker=
"disc"
size=
"sm
"
>
<
ListItem
>
<
ul
className=
"text-sm list-disc ml-4 space-y-1
"
>
<
li
>
<
Link
className=
"text-sm text-blue-600 hover:underline"
to=
"https://www.usememos.com/docs/advanced-settings/local-storage"
...
...
@@ -210,8 +233,8 @@ const StorageSection = observer(() => {
>
Docs - Local storage
</
Link
>
</
ListItem
>
<
ListItem
>
</
li
>
<
li
>
<
Link
className=
"text-sm text-blue-600 hover:underline"
to=
"https://www.usememos.com/blog/choosing-a-storage-for-your-resource"
...
...
@@ -219,8 +242,8 @@ const StorageSection = observer(() => {
>
Choosing a Storage for Your Resource: Database, S3 or Local Storage?
</
Link
>
</
ListItem
>
</
List
>
</
li
>
</
ul
>
</
div
>
</
div
>
);
...
...
web/src/components/Settings/UserSessionsSection.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
{
ClockIcon
,
MonitorIcon
,
SmartphoneIcon
,
TabletIcon
,
TrashIcon
,
WifiIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
userServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
UserSession
}
from
"@/types/proto/api/v1/user_service"
;
...
...
@@ -124,7 +124,7 @@ const UserSessionsSection = () => {
</
td
>
<
td
className=
"relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm"
>
<
Button
variant=
"
plain
"
variant=
"
ghost
"
disabled=
{
isCurrentSession
(
userSession
)
}
onClick=
{
()
=>
{
handleRevokeSession
(
userSession
);
...
...
web/src/components/Settings/WebhookSection.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
{
ExternalLinkIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
Link
}
from
"react-router-dom"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
webhookServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
Webhook
}
from
"@/types/proto/api/v1/webhook_service"
;
...
...
@@ -85,7 +85,7 @@ const WebhookSection = () => {
</
td
>
<
td
className=
"relative whitespace-nowrap px-3 py-2 text-right text-sm"
>
<
Button
variant=
"
plain
"
variant=
"
ghost
"
onClick=
{
()
=>
{
handleDeleteWebhook
(
webhook
);
}
}
...
...
web/src/components/Settings/WorkspaceSection.tsx
View file @
493832ae
import
{
Select
,
Option
,
Divider
}
from
"@mui/joy"
;
import
{
Button
,
Textarea
,
Switch
}
from
"@usememos/mui"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
ExternalLinkIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Link
}
from
"react-router-dom"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
workspaceSettingNamePrefix
}
from
"@/store/common"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
...
...
@@ -71,19 +74,18 @@ const WorkspaceSection = observer(() => {
{
t
(
"setting.system-section.server-name"
)
}
:
{
" "
}
<
span
className=
"font-mono font-bold"
>
{
workspaceGeneralSetting
.
customProfile
?.
title
||
"Memos"
}
</
span
>
</
div
>
<
Button
variant=
"outline
d
"
onClick=
{
handleUpdateCustomizedProfileButtonClick
}
>
<
Button
variant=
"outline"
onClick=
{
handleUpdateCustomizedProfileButtonClick
}
>
{
t
(
"common.edit"
)
}
</
Button
>
</
div
>
<
Divide
r
/>
<
Separato
r
/>
<
p
className=
"font-medium text-gray-700 dark:text-gray-500"
>
{
t
(
"setting.system-section.title"
)
}
</
p
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.system-section.additional-style"
)
}
</
span
>
</
div
>
<
Textarea
className=
"font-mono"
className=
"font-mono
w-full
"
rows=
{
3
}
fullWidth
placeholder=
{
t
(
"setting.system-section.additional-style-placeholder"
)
}
value=
{
workspaceGeneralSetting
.
additionalStyle
}
onChange=
{
(
event
)
=>
updatePartialSetting
({
additionalStyle
:
event
.
target
.
value
})
}
...
...
@@ -92,9 +94,8 @@ const WorkspaceSection = observer(() => {
<
span
>
{
t
(
"setting.system-section.additional-script"
)
}
</
span
>
</
div
>
<
Textarea
className=
"font-mono"
className=
"font-mono
w-full
"
rows=
{
3
}
fullWidth
placeholder=
{
t
(
"setting.system-section.additional-script-placeholder"
)
}
value=
{
workspaceGeneralSetting
.
additionalScript
}
onChange=
{
(
event
)
=>
updatePartialSetting
({
additionalScript
:
event
.
target
.
value
})
}
...
...
@@ -114,7 +115,7 @@ const WorkspaceSection = observer(() => {
<
Switch
disabled=
{
workspaceStore
.
state
.
profile
.
mode
===
"demo"
}
checked=
{
workspaceGeneralSetting
.
disallowUserRegistration
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
disallowUserRegistration
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowUserRegistration
:
checked
})
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
...
...
@@ -125,39 +126,43 @@ const WorkspaceSection = observer(() => {
(
identityProviderList
.
length
===
0
&&
!
workspaceGeneralSetting
.
disallowPasswordAuth
)
}
checked=
{
workspaceGeneralSetting
.
disallowPasswordAuth
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
disallowPasswordAuth
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowPasswordAuth
:
checked
})
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.workspace-section.disallow-change-username"
)
}
</
span
>
<
Switch
checked=
{
workspaceGeneralSetting
.
disallowChangeUsername
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
disallowChangeUsername
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowChangeUsername
:
checked
})
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
>
{
t
(
"setting.workspace-section.disallow-change-nickname"
)
}
</
span
>
<
Switch
checked=
{
workspaceGeneralSetting
.
disallowChangeNickname
}
onCh
ange=
{
(
event
)
=>
updatePartialSetting
({
disallowChangeNickname
:
event
.
target
.
checked
})
}
onCh
eckedChange=
{
(
checked
)
=>
updatePartialSetting
({
disallowChangeNickname
:
checked
})
}
/>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"truncate"
>
{
t
(
"setting.workspace-section.week-start-day"
)
}
</
span
>
<
Select
className=
"min-w-fit!"
value=
{
workspaceGeneralSetting
.
weekStartDayOffset
}
onChange=
{
(
_
,
weekStartDayOffset
)
=>
{
updatePartialSetting
({
weekStartDayOffset
:
weekStartDayOffset
||
0
});
value=
{
workspaceGeneralSetting
.
weekStartDayOffset
.
toString
()
}
onValueChange=
{
(
value
)
=>
{
updatePartialSetting
({
weekStartDayOffset
:
parseInt
(
value
)
||
0
});
}
}
>
<
Option
value=
{
-
1
}
>
{
t
(
"setting.workspace-section.saturday"
)
}
</
Option
>
<
Option
value=
{
0
}
>
{
t
(
"setting.workspace-section.sunday"
)
}
</
Option
>
<
Option
value=
{
1
}
>
{
t
(
"setting.workspace-section.monday"
)
}
</
Option
>
<
SelectTrigger
className=
"min-w-fit"
>
<
SelectValue
/>
</
SelectTrigger
>
<
SelectContent
>
<
SelectItem
value=
"-1"
>
{
t
(
"setting.workspace-section.saturday"
)
}
</
SelectItem
>
<
SelectItem
value=
"0"
>
{
t
(
"setting.workspace-section.sunday"
)
}
</
SelectItem
>
<
SelectItem
value=
"1"
>
{
t
(
"setting.workspace-section.monday"
)
}
</
SelectItem
>
</
SelectContent
>
</
Select
>
</
div
>
<
div
className=
"mt-2 w-full flex justify-end"
>
<
Button
color=
"primary"
disabled=
{
isEqual
(
workspaceGeneralSetting
,
originalSetting
)
}
onClick=
{
handleSaveGeneralSetting
}
>
<
Button
disabled=
{
isEqual
(
workspaceGeneralSetting
,
originalSetting
)
}
onClick=
{
handleSaveGeneralSetting
}
>
{
t
(
"common.save"
)
}
</
Button
>
</
div
>
...
...
web/src/components/StatisticsView/StatCard.tsx
View file @
493832ae
import
{
Tooltip
}
from
"@mui/joy"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
cn
}
from
"@/lib/utils"
;
import
type
{
StatCardProps
}
from
"@/types/statistics"
;
import
{
cn
}
from
"@/utils"
;
export
const
StatCard
=
({
icon
,
label
,
count
,
onClick
,
tooltip
,
className
}:
StatCardProps
)
=>
{
const
content
=
(
...
...
@@ -22,9 +22,14 @@ export const StatCard = ({ icon, label, count, onClick, tooltip, className }: St
if
(
tooltip
)
{
return
(
<
Tooltip
title=
{
tooltip
}
placement=
"top"
arrow
>
{
content
}
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
{
content
}
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
{
tooltip
}
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
);
}
...
...
web/src/components/UpdateAccountDialog.tsx
View file @
493832ae
import
{
Button
,
Input
,
Textarea
}
from
"@usememos/mui"
;
import
{
isEqual
}
from
"lodash-es"
;
import
{
XIcon
}
from
"lucide-react"
;
import
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
convertFileToBase64
}
from
"@/helpers/utils"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
userStore
,
workspaceStore
}
from
"@/store/v2"
;
...
...
@@ -142,7 +144,7 @@ const UpdateAccountDialog = ({ destroy }: Props) => {
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
t
(
"setting.account-section.update-information"
)
}
</
p
>
<
Button
variant=
"
plain
"
onClick=
{
handleCloseBtnClick
}
>
<
Button
variant=
"
ghost
"
onClick=
{
handleCloseBtnClick
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
...
...
@@ -188,16 +190,14 @@ const UpdateAccountDialog = ({ destroy }: Props) => {
{
t
(
"common.email"
)
}
<
span
className=
"text-sm text-gray-400 ml-1"
>
(
{
t
(
"setting.account-section.email-note"
)
}
)
</
span
>
</
p
>
<
Input
fullWidth
type=
"email"
value=
{
state
.
email
}
onChange=
{
handleEmailChanged
}
/>
<
Input
className=
"w-full"
type=
"email"
value=
{
state
.
email
}
onChange=
{
handleEmailChanged
}
/>
<
p
className=
"text-sm"
>
{
t
(
"common.description"
)
}
</
p
>
<
Textarea
rows=
{
2
}
fullWidth
value=
{
state
.
description
}
onChange=
{
handleDescriptionChanged
}
/>
<
Textarea
className=
"w-full"
rows=
{
2
}
value=
{
state
.
description
}
onChange=
{
handleDescriptionChanged
}
/>
<
div
className=
"w-full flex flex-row justify-end items-center pt-4 space-x-2"
>
<
Button
variant=
"
plain
"
onClick=
{
handleCloseBtnClick
}
>
<
Button
variant=
"
ghost
"
onClick=
{
handleCloseBtnClick
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"common.save"
)
}
</
Button
>
<
Button
onClick=
{
handleSaveBtnClick
}
>
{
t
(
"common.save"
)
}
</
Button
>
</
div
>
</
div
>
</
div
>
...
...
web/src/components/UpdateCustomizedProfileDialog.tsx
View file @
493832ae
import
{
Button
,
Input
,
Textarea
}
from
"@usememos/mui"
;
import
{
XIcon
}
from
"lucide-react"
;
import
{
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
workspaceSettingNamePrefix
}
from
"@/store/common"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
{
WorkspaceSettingKey
}
from
"@/store/v2/workspace"
;
...
...
@@ -99,7 +101,7 @@ const UpdateCustomizedProfileDialog = ({ destroy }: Props) => {
<
div
className=
"max-w-full shadow flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-300 p-4 rounded-lg"
>
<
div
className=
"flex flex-row justify-between items-center mb-4 gap-2 w-full"
>
<
p
className=
"title-text"
>
{
t
(
"setting.system-section.customize-server.title"
)
}
</
p
>
<
Button
variant=
"
plain
"
onClick=
{
handleCloseButtonClick
}
>
<
Button
variant=
"
ghost
"
onClick=
{
handleCloseButtonClick
}
>
<
XIcon
className=
"w-5 h-auto"
/>
</
Button
>
</
div
>
...
...
@@ -109,19 +111,19 @@ const UpdateCustomizedProfileDialog = ({ destroy }: Props) => {
<
p
className=
"text-sm mb-1 mt-2"
>
{
t
(
"setting.system-section.customize-server.icon-url"
)
}
</
p
>
<
Input
className=
"w-full"
type=
"text"
value=
{
customProfile
.
logoUrl
}
onChange=
{
handleLogoUrlChanged
}
/>
<
p
className=
"text-sm mb-1 mt-2"
>
{
t
(
"setting.system-section.customize-server.description"
)
}
</
p
>
<
Textarea
rows=
{
3
}
fullWidth
value=
{
customProfile
.
description
}
onChange=
{
handleDescriptionChanged
}
/>
<
Textarea
rows=
{
3
}
value=
{
customProfile
.
description
}
onChange=
{
handleDescriptionChanged
}
/>
<
p
className=
"text-sm mb-1 mt-2"
>
{
t
(
"setting.system-section.customize-server.locale"
)
}
</
p
>
<
LocaleSelect
className=
"w-full!"
value=
{
customProfile
.
locale
}
onChange=
{
handleLocaleSelectChange
}
/>
<
p
className=
"text-sm mb-1 mt-2"
>
{
t
(
"setting.system-section.customize-server.appearance"
)
}
</
p
>
<
AppearanceSelect
className=
"w-full!"
value=
{
customProfile
.
appearance
as
Appearance
}
onChange=
{
handleAppearanceSelectChange
}
/>
<
div
className=
"mt-4 w-full flex flex-row justify-between items-center space-x-2"
>
<
div
className=
"flex flex-row justify-start items-center"
>
<
Button
variant=
"outline
d
"
onClick=
{
handleRestoreButtonClick
}
>
<
Button
variant=
"outline"
onClick=
{
handleRestoreButtonClick
}
>
{
t
(
"common.restore"
)
}
</
Button
>
</
div
>
<
div
className=
"flex flex-row justify-end items-center gap-2"
>
<
Button
variant=
"
plain
"
onClick=
{
handleCloseButtonClick
}
>
<
Button
variant=
"
ghost
"
onClick=
{
handleCloseButtonClick
}
>
{
t
(
"common.cancel"
)
}
</
Button
>
<
Button
color=
"primary"
onClick=
{
handleSaveButtonClick
}
>
...
...
web/src/components/UserAvatar.tsx
View file @
493832ae
import
{
cn
}
from
"@/utils"
;
import
{
cn
}
from
"@/
lib/
utils"
;
interface
Props
{
avatarUrl
?:
string
;
...
...
web/src/components/UserBanner.tsx
View file @
493832ae
...
...
@@ -2,11 +2,11 @@ import { ArchiveIcon, LogOutIcon, User2Icon, SquareUserIcon, SettingsIcon, BellI
import
{
authServiceClient
}
from
"@/grpcweb"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Routes
}
from
"@/router"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
UserAvatar
from
"./UserAvatar"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"./ui/
P
opover"
;
import
{
Popover
,
PopoverContent
,
PopoverTrigger
}
from
"./ui/
p
opover"
;
interface
Props
{
collapsed
?:
boolean
;
...
...
web/src/components/VisibilityIcon.tsx
View file @
493832ae
import
{
Globe2Icon
,
LockIcon
,
UsersIcon
}
from
"lucide-react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Visibility
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
interface
Props
{
visibility
:
Visibility
;
...
...
web/src/components/kit/OverflowTip.tsx
View file @
493832ae
import
{
Tooltip
}
from
"@mui/joy"
;
import
{
useRef
,
useState
,
useEffect
}
from
"react"
;
import
{
cn
}
from
"@/utils"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
cn
}
from
"@/lib/utils"
;
interface
Props
{
children
:
React
.
ReactNode
;
...
...
@@ -20,11 +20,20 @@ const OverflowTip = ({ children, className }: Props) => {
},
[]);
return
(
<
Tooltip
title=
{
children
}
placement=
"top"
arrow
disableHoverListener=
{
!
isOverflowed
}
>
<
div
ref=
{
textElementRef
}
className=
{
cn
(
"truncate"
,
className
)
}
>
{
children
}
</
div
>
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
div
ref=
{
textElementRef
}
className=
{
cn
(
"truncate"
,
className
)
}
>
{
children
}
</
div
>
</
TooltipTrigger
>
{
isOverflowed
&&
(
<
TooltipContent
>
<
p
>
{
children
}
</
p
>
</
TooltipContent
>
)
}
</
Tooltip
>
</
TooltipProvider
>
);
};
...
...
web/src/components/ui/Popover.tsx
View file @
493832ae
import
*
as
PopoverPrimitive
from
"@radix-ui/react-popover"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/utils"
;
import
{
cn
}
from
"@/
lib/
utils"
;
const
Popover
=
PopoverPrimitive
.
Root
;
function
Popover
({
...
props
}:
React
.
ComponentProps
<
typeof
PopoverPrimitive
.
Root
>
)
{
return
<
PopoverPrimitive
.
Root
data
-
slot=
"popover"
{
...
props
}
/>;
}
const
PopoverTrigger
=
PopoverPrimitive
.
Trigger
;
function
PopoverTrigger
({
...
props
}:
React
.
ComponentProps
<
typeof
PopoverPrimitive
.
Trigger
>
)
{
return
<
PopoverPrimitive
.
Trigger
data
-
slot=
"popover-trigger"
{
...
props
}
/>;
}
const
PopoverContent
=
React
.
forwardRef
<
React
.
ElementRef
<
typeof
PopoverPrimitive
.
Content
>
,
React
.
ComponentPropsWithoutRef
<
typeof
PopoverPrimitive
.
Content
>
>
(({
className
,
align
=
"center"
,
sideOffset
=
4
,
...
props
},
ref
)
=>
(
<
PopoverPrimitive
.
Portal
>
<
PopoverPrimitive
.
Content
ref=
{
ref
}
align=
{
align
}
sideOffset=
{
sideOffset
}
className=
{
cn
(
"z-2000 w-auto rounded-md bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 p-1 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
,
className
,
)
}
{
...
props
}
/>
</
PopoverPrimitive
.
Portal
>
));
PopoverContent
.
displayName
=
PopoverPrimitive
.
Content
.
displayName
;
function
PopoverContent
({
className
,
align
=
"center"
,
sideOffset
=
4
,
...
props
}:
React
.
ComponentProps
<
typeof
PopoverPrimitive
.
Content
>
)
{
return
(
<
PopoverPrimitive
.
Portal
>
<
PopoverPrimitive
.
Content
data
-
slot=
"popover-content"
align=
{
align
}
sideOffset=
{
sideOffset
}
className=
{
cn
(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-auto origin-(--radix-popover-content-transform-origin) rounded-md border p-1 shadow-md outline-hidden"
,
className
,
)
}
{
...
props
}
/>
</
PopoverPrimitive
.
Portal
>
);
}
export
{
Popover
,
PopoverTrigger
,
PopoverContent
};
function
PopoverAnchor
({
...
props
}:
React
.
ComponentProps
<
typeof
PopoverPrimitive
.
Anchor
>
)
{
return
<
PopoverPrimitive
.
Anchor
data
-
slot=
"popover-anchor"
{
...
props
}
/>;
}
export
{
Popover
,
PopoverTrigger
,
PopoverContent
,
PopoverAnchor
};
web/src/components/ui/badge.tsx
0 → 100644
View file @
493832ae
import
{
Slot
}
from
"@radix-ui/react-slot"
;
import
{
cva
,
type
VariantProps
}
from
"class-variance-authority"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
const
badgeVariants
=
cva
(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden"
,
{
variants
:
{
variant
:
{
default
:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90"
,
secondary
:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90"
,
destructive
:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60"
,
outline
:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground"
,
},
},
defaultVariants
:
{
variant
:
"default"
,
},
},
);
function
Badge
({
className
,
variant
,
asChild
=
false
,
...
props
}:
React
.
ComponentProps
<
"span"
>
&
VariantProps
<
typeof
badgeVariants
>
&
{
asChild
?:
boolean
})
{
const
Comp
=
asChild
?
Slot
:
"span"
;
return
<
Comp
data
-
slot=
"badge"
className=
{
cn
(
badgeVariants
({
variant
}),
className
)
}
{
...
props
}
/>;
}
export
{
Badge
,
badgeVariants
};
web/src/components/ui/button.tsx
0 → 100644
View file @
493832ae
import
{
Slot
}
from
"@radix-ui/react-slot"
;
import
{
cva
,
type
VariantProps
}
from
"class-variance-authority"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
const
buttonVariants
=
cva
(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
,
{
variants
:
{
variant
:
{
default
:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90"
,
destructive
:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60"
,
outline
:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50"
,
secondary
:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80"
,
ghost
:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50"
,
link
:
"text-primary underline-offset-4 hover:underline"
,
},
size
:
{
default
:
"h-8 px-3 py-2 has-[>svg]:px-3"
,
sm
:
"h-7 rounded-md gap-1 px-2 has-[>svg]:px-2"
,
lg
:
"h-9 rounded-md px-4 has-[>svg]:px-3"
,
icon
:
"size-8"
,
},
},
defaultVariants
:
{
variant
:
"default"
,
size
:
"default"
,
},
},
);
const
Button
=
React
.
forwardRef
<
HTMLButtonElement
,
React
.
ComponentProps
<
"button"
>
&
VariantProps
<
typeof
buttonVariants
>
&
{
asChild
?:
boolean
;
}
>
(({
className
,
variant
,
size
,
asChild
=
false
,
...
props
},
ref
)
=>
{
const
Comp
=
asChild
?
Slot
:
"button"
;
return
<
Comp
ref=
{
ref
}
data
-
slot=
"button"
className=
{
cn
(
buttonVariants
({
variant
,
size
,
className
}))
}
{
...
props
}
/>;
});
Button
.
displayName
=
"Button"
;
export
{
Button
,
buttonVariants
};
web/src/components/ui/checkbox.tsx
0 → 100644
View file @
493832ae
import
*
as
CheckboxPrimitive
from
"@radix-ui/react-checkbox"
;
import
{
CheckIcon
}
from
"lucide-react"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Checkbox
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
CheckboxPrimitive
.
Root
>
)
{
return
(
<
CheckboxPrimitive
.
Root
data
-
slot=
"checkbox"
className=
{
cn
(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
,
className
,
)
}
{
...
props
}
>
<
CheckboxPrimitive
.
Indicator
data
-
slot=
"checkbox-indicator"
className=
"flex items-center justify-center text-current transition-none"
>
<
CheckIcon
className=
"size-3.5"
/>
</
CheckboxPrimitive
.
Indicator
>
</
CheckboxPrimitive
.
Root
>
);
}
export
{
Checkbox
};
web/src/components/ui/command.tsx
0 → 100644
View file @
493832ae
import
{
Command
as
CommandPrimitive
}
from
"cmdk"
;
import
{
SearchIcon
}
from
"lucide-react"
;
import
*
as
React
from
"react"
;
import
{
Dialog
,
DialogContent
,
DialogDescription
,
DialogHeader
,
DialogTitle
}
from
"@/components/ui/dialog"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Command
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
CommandPrimitive
>
)
{
return
(
<
CommandPrimitive
data
-
slot=
"command"
className=
{
cn
(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md"
,
className
)
}
{
...
props
}
/>
);
}
function
CommandDialog
({
title
=
"Command Palette"
,
description
=
"Search for a command to run..."
,
children
,
className
,
showCloseButton
=
true
,
...
props
}:
React
.
ComponentProps
<
typeof
Dialog
>
&
{
title
?:
string
;
description
?:
string
;
className
?:
string
;
showCloseButton
?:
boolean
;
})
{
return
(
<
Dialog
{
...
props
}
>
<
DialogHeader
className=
"sr-only"
>
<
DialogTitle
>
{
title
}
</
DialogTitle
>
<
DialogDescription
>
{
description
}
</
DialogDescription
>
</
DialogHeader
>
<
DialogContent
className=
{
cn
(
"overflow-hidden p-0"
,
className
)
}
showCloseButton=
{
showCloseButton
}
>
<
Command
className=
"[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
>
{
children
}
</
Command
>
</
DialogContent
>
</
Dialog
>
);
}
function
CommandInput
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
CommandPrimitive
.
Input
>
)
{
return
(
<
div
data
-
slot=
"command-input-wrapper"
className=
"flex h-9 items-center gap-2 border-b px-3"
>
<
SearchIcon
className=
"size-4 shrink-0 opacity-50"
/>
<
CommandPrimitive
.
Input
data
-
slot=
"command-input"
className=
{
cn
(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
,
className
,
)
}
{
...
props
}
/>
</
div
>
);
}
function
CommandList
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
CommandPrimitive
.
List
>
)
{
return
(
<
CommandPrimitive
.
List
data
-
slot=
"command-list"
className=
{
cn
(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto"
,
className
)
}
{
...
props
}
/>
);
}
function
CommandEmpty
({
...
props
}:
React
.
ComponentProps
<
typeof
CommandPrimitive
.
Empty
>
)
{
return
<
CommandPrimitive
.
Empty
data
-
slot=
"command-empty"
className=
"py-6 text-center text-sm"
{
...
props
}
/>;
}
function
CommandGroup
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
CommandPrimitive
.
Group
>
)
{
return
(
<
CommandPrimitive
.
Group
data
-
slot=
"command-group"
className=
{
cn
(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium"
,
className
,
)
}
{
...
props
}
/>
);
}
function
CommandSeparator
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
CommandPrimitive
.
Separator
>
)
{
return
<
CommandPrimitive
.
Separator
data
-
slot=
"command-separator"
className=
{
cn
(
"bg-border -mx-1 h-px"
,
className
)
}
{
...
props
}
/>;
}
function
CommandItem
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
CommandPrimitive
.
Item
>
)
{
return
(
<
CommandPrimitive
.
Item
data
-
slot=
"command-item"
className=
{
cn
(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
,
className
,
)
}
{
...
props
}
/>
);
}
function
CommandShortcut
({
className
,
...
props
}:
React
.
ComponentProps
<
"span"
>
)
{
return
(
<
span
data
-
slot=
"command-shortcut"
className=
{
cn
(
"text-muted-foreground ml-auto text-xs tracking-widest"
,
className
)
}
{
...
props
}
/>
);
}
export
{
Command
,
CommandDialog
,
CommandInput
,
CommandList
,
CommandEmpty
,
CommandGroup
,
CommandItem
,
CommandShortcut
,
CommandSeparator
};
web/src/components/ui/dialog.tsx
0 → 100644
View file @
493832ae
import
*
as
DialogPrimitive
from
"@radix-ui/react-dialog"
;
import
{
XIcon
}
from
"lucide-react"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Dialog
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Root
>
)
{
return
<
DialogPrimitive
.
Root
data
-
slot=
"dialog"
{
...
props
}
/>;
}
function
DialogTrigger
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Trigger
>
)
{
return
<
DialogPrimitive
.
Trigger
data
-
slot=
"dialog-trigger"
{
...
props
}
/>;
}
function
DialogPortal
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Portal
>
)
{
return
<
DialogPrimitive
.
Portal
data
-
slot=
"dialog-portal"
{
...
props
}
/>;
}
function
DialogClose
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Close
>
)
{
return
<
DialogPrimitive
.
Close
data
-
slot=
"dialog-close"
{
...
props
}
/>;
}
function
DialogOverlay
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Overlay
>
)
{
return
(
<
DialogPrimitive
.
Overlay
data
-
slot=
"dialog-overlay"
className=
{
cn
(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50"
,
className
,
)
}
{
...
props
}
/>
);
}
function
DialogContent
({
className
,
children
,
showCloseButton
=
true
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Content
>
&
{
showCloseButton
?:
boolean
;
})
{
return
(
<
DialogPortal
data
-
slot=
"dialog-portal"
>
<
DialogOverlay
/>
<
DialogPrimitive
.
Content
data
-
slot=
"dialog-content"
className=
{
cn
(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg"
,
className
,
)
}
{
...
props
}
>
{
children
}
{
showCloseButton
&&
(
<
DialogPrimitive
.
Close
data
-
slot=
"dialog-close"
className=
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<
XIcon
/>
<
span
className=
"sr-only"
>
Close
</
span
>
</
DialogPrimitive
.
Close
>
)
}
</
DialogPrimitive
.
Content
>
</
DialogPortal
>
);
}
function
DialogHeader
({
className
,
...
props
}:
React
.
ComponentProps
<
"div"
>
)
{
return
<
div
data
-
slot=
"dialog-header"
className=
{
cn
(
"flex flex-col gap-2 text-center sm:text-left"
,
className
)
}
{
...
props
}
/>;
}
function
DialogFooter
({
className
,
...
props
}:
React
.
ComponentProps
<
"div"
>
)
{
return
<
div
data
-
slot=
"dialog-footer"
className=
{
cn
(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"
,
className
)
}
{
...
props
}
/>;
}
function
DialogTitle
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Title
>
)
{
return
<
DialogPrimitive
.
Title
data
-
slot=
"dialog-title"
className=
{
cn
(
"text-lg leading-none font-semibold"
,
className
)
}
{
...
props
}
/>;
}
function
DialogDescription
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Description
>
)
{
return
(
<
DialogPrimitive
.
Description
data
-
slot=
"dialog-description"
className=
{
cn
(
"text-muted-foreground text-sm"
,
className
)
}
{
...
props
}
/>
);
}
export
{
Dialog
,
DialogClose
,
DialogContent
,
DialogDescription
,
DialogFooter
,
DialogHeader
,
DialogOverlay
,
DialogPortal
,
DialogTitle
,
DialogTrigger
,
};
web/src/components/ui/input.tsx
0 → 100644
View file @
493832ae
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Input
({
className
,
type
,
...
props
}:
React
.
ComponentProps
<
"input"
>
)
{
return
(
<
input
type=
{
type
}
data
-
slot=
"input"
className=
{
cn
(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
,
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
,
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
,
className
,
)
}
{
...
props
}
/>
);
}
export
{
Input
};
web/src/components/ui/label.tsx
0 → 100644
View file @
493832ae
import
*
as
LabelPrimitive
from
"@radix-ui/react-label"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Label
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
LabelPrimitive
.
Root
>
)
{
return
(
<
LabelPrimitive
.
Root
data
-
slot=
"label"
className=
{
cn
(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
,
className
,
)
}
{
...
props
}
/>
);
}
export
{
Label
};
web/src/components/ui/radio-group.tsx
0 → 100644
View file @
493832ae
import
*
as
RadioGroupPrimitive
from
"@radix-ui/react-radio-group"
;
import
{
CircleIcon
}
from
"lucide-react"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
RadioGroup
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
RadioGroupPrimitive
.
Root
>
)
{
return
<
RadioGroupPrimitive
.
Root
data
-
slot=
"radio-group"
className=
{
cn
(
"grid gap-3"
,
className
)
}
{
...
props
}
/>;
}
function
RadioGroupItem
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
RadioGroupPrimitive
.
Item
>
)
{
return
(
<
RadioGroupPrimitive
.
Item
data
-
slot=
"radio-group-item"
className=
{
cn
(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
,
className
,
)
}
{
...
props
}
>
<
RadioGroupPrimitive
.
Indicator
data
-
slot=
"radio-group-indicator"
className=
"relative flex items-center justify-center"
>
<
CircleIcon
className=
"fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2"
/>
</
RadioGroupPrimitive
.
Indicator
>
</
RadioGroupPrimitive
.
Item
>
);
}
export
{
RadioGroup
,
RadioGroupItem
};
web/src/components/ui/select.tsx
0 → 100644
View file @
493832ae
import
*
as
SelectPrimitive
from
"@radix-ui/react-select"
;
import
{
CheckIcon
,
ChevronDownIcon
,
ChevronUpIcon
}
from
"lucide-react"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Select
({
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Root
>
)
{
return
<
SelectPrimitive
.
Root
data
-
slot=
"select"
{
...
props
}
/>;
}
function
SelectGroup
({
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Group
>
)
{
return
<
SelectPrimitive
.
Group
data
-
slot=
"select-group"
{
...
props
}
/>;
}
function
SelectValue
({
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Value
>
)
{
return
<
SelectPrimitive
.
Value
data
-
slot=
"select-value"
{
...
props
}
/>;
}
function
SelectTrigger
({
className
,
size
=
"default"
,
children
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Trigger
>
&
{
size
?:
"sm"
|
"default"
;
})
{
return
(
<
SelectPrimitive
.
Trigger
data
-
slot=
"select-trigger"
data
-
size=
{
size
}
className=
{
cn
(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
,
className
,
)
}
{
...
props
}
>
{
children
}
<
SelectPrimitive
.
Icon
asChild
>
<
ChevronDownIcon
className=
"size-4 opacity-50"
/>
</
SelectPrimitive
.
Icon
>
</
SelectPrimitive
.
Trigger
>
);
}
function
SelectContent
({
className
,
children
,
position
=
"popper"
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Content
>
)
{
return
(
<
SelectPrimitive
.
Portal
>
<
SelectPrimitive
.
Content
data
-
slot=
"select-content"
className=
{
cn
(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md"
,
position
===
"popper"
&&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1"
,
className
,
)
}
position=
{
position
}
{
...
props
}
>
<
SelectScrollUpButton
/>
<
SelectPrimitive
.
Viewport
className=
{
cn
(
"p-1"
,
position
===
"popper"
&&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
,
)
}
>
{
children
}
</
SelectPrimitive
.
Viewport
>
<
SelectScrollDownButton
/>
</
SelectPrimitive
.
Content
>
</
SelectPrimitive
.
Portal
>
);
}
function
SelectLabel
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Label
>
)
{
return
(
<
SelectPrimitive
.
Label
data
-
slot=
"select-label"
className=
{
cn
(
"text-muted-foreground px-2 py-1.5 text-xs"
,
className
)
}
{
...
props
}
/>
);
}
function
SelectItem
({
className
,
children
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Item
>
)
{
return
(
<
SelectPrimitive
.
Item
data
-
slot=
"select-item"
className=
{
cn
(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2"
,
className
,
)
}
{
...
props
}
>
<
span
className=
"absolute right-2 flex size-3.5 items-center justify-center"
>
<
SelectPrimitive
.
ItemIndicator
>
<
CheckIcon
className=
"size-4"
/>
</
SelectPrimitive
.
ItemIndicator
>
</
span
>
<
SelectPrimitive
.
ItemText
>
{
children
}
</
SelectPrimitive
.
ItemText
>
</
SelectPrimitive
.
Item
>
);
}
function
SelectSeparator
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
Separator
>
)
{
return
(
<
SelectPrimitive
.
Separator
data
-
slot=
"select-separator"
className=
{
cn
(
"bg-border pointer-events-none -mx-1 my-1 h-px"
,
className
)
}
{
...
props
}
/>
);
}
function
SelectScrollUpButton
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
ScrollUpButton
>
)
{
return
(
<
SelectPrimitive
.
ScrollUpButton
data
-
slot=
"select-scroll-up-button"
className=
{
cn
(
"flex cursor-default items-center justify-center py-1"
,
className
)
}
{
...
props
}
>
<
ChevronUpIcon
className=
"size-4"
/>
</
SelectPrimitive
.
ScrollUpButton
>
);
}
function
SelectScrollDownButton
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SelectPrimitive
.
ScrollDownButton
>
)
{
return
(
<
SelectPrimitive
.
ScrollDownButton
data
-
slot=
"select-scroll-down-button"
className=
{
cn
(
"flex cursor-default items-center justify-center py-1"
,
className
)
}
{
...
props
}
>
<
ChevronDownIcon
className=
"size-4"
/>
</
SelectPrimitive
.
ScrollDownButton
>
);
}
export
{
Select
,
SelectContent
,
SelectGroup
,
SelectItem
,
SelectLabel
,
SelectScrollDownButton
,
SelectScrollUpButton
,
SelectSeparator
,
SelectTrigger
,
SelectValue
,
};
web/src/components/ui/separator.tsx
0 → 100644
View file @
493832ae
import
*
as
SeparatorPrimitive
from
"@radix-ui/react-separator"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Separator
({
className
,
orientation
=
"horizontal"
,
decorative
=
true
,
...
props
}:
React
.
ComponentProps
<
typeof
SeparatorPrimitive
.
Root
>
)
{
return
(
<
SeparatorPrimitive
.
Root
data
-
slot=
"separator"
decorative=
{
decorative
}
orientation=
{
orientation
}
className=
{
cn
(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px"
,
className
,
)
}
{
...
props
}
/>
);
}
export
{
Separator
};
web/src/components/ui/sheet.tsx
0 → 100644
View file @
493832ae
import
*
as
SheetPrimitive
from
"@radix-ui/react-dialog"
;
import
{
XIcon
}
from
"lucide-react"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Sheet
({
...
props
}:
React
.
ComponentProps
<
typeof
SheetPrimitive
.
Root
>
)
{
return
<
SheetPrimitive
.
Root
data
-
slot=
"sheet"
{
...
props
}
/>;
}
function
SheetTrigger
({
...
props
}:
React
.
ComponentProps
<
typeof
SheetPrimitive
.
Trigger
>
)
{
return
<
SheetPrimitive
.
Trigger
data
-
slot=
"sheet-trigger"
{
...
props
}
/>;
}
function
SheetClose
({
...
props
}:
React
.
ComponentProps
<
typeof
SheetPrimitive
.
Close
>
)
{
return
<
SheetPrimitive
.
Close
data
-
slot=
"sheet-close"
{
...
props
}
/>;
}
function
SheetPortal
({
...
props
}:
React
.
ComponentProps
<
typeof
SheetPrimitive
.
Portal
>
)
{
return
<
SheetPrimitive
.
Portal
data
-
slot=
"sheet-portal"
{
...
props
}
/>;
}
function
SheetOverlay
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SheetPrimitive
.
Overlay
>
)
{
return
(
<
SheetPrimitive
.
Overlay
data
-
slot=
"sheet-overlay"
className=
{
cn
(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50"
,
className
,
)
}
{
...
props
}
/>
);
}
function
SheetContent
({
className
,
children
,
side
=
"right"
,
...
props
}:
React
.
ComponentProps
<
typeof
SheetPrimitive
.
Content
>
&
{
side
?:
"top"
|
"right"
|
"bottom"
|
"left"
;
})
{
return
(
<
SheetPortal
>
<
SheetOverlay
/>
<
SheetPrimitive
.
Content
data
-
slot=
"sheet-content"
className=
{
cn
(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500"
,
side
===
"right"
&&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm"
,
side
===
"left"
&&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm"
,
side
===
"top"
&&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b"
,
side
===
"bottom"
&&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t"
,
className
,
)
}
{
...
props
}
>
{
children
}
<
SheetPrimitive
.
Close
className=
"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
>
<
XIcon
className=
"size-4"
/>
<
span
className=
"sr-only"
>
Close
</
span
>
</
SheetPrimitive
.
Close
>
</
SheetPrimitive
.
Content
>
</
SheetPortal
>
);
}
function
SheetHeader
({
className
,
...
props
}:
React
.
ComponentProps
<
"div"
>
)
{
return
<
div
data
-
slot=
"sheet-header"
className=
{
cn
(
"flex flex-col gap-1.5 p-4"
,
className
)
}
{
...
props
}
/>;
}
function
SheetFooter
({
className
,
...
props
}:
React
.
ComponentProps
<
"div"
>
)
{
return
<
div
data
-
slot=
"sheet-footer"
className=
{
cn
(
"mt-auto flex flex-col gap-2 p-4"
,
className
)
}
{
...
props
}
/>;
}
function
SheetTitle
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SheetPrimitive
.
Title
>
)
{
return
<
SheetPrimitive
.
Title
data
-
slot=
"sheet-title"
className=
{
cn
(
"text-foreground font-semibold"
,
className
)
}
{
...
props
}
/>;
}
function
SheetDescription
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SheetPrimitive
.
Description
>
)
{
return
<
SheetPrimitive
.
Description
data
-
slot=
"sheet-description"
className=
{
cn
(
"text-muted-foreground text-sm"
,
className
)
}
{
...
props
}
/>;
}
export
{
Sheet
,
SheetTrigger
,
SheetClose
,
SheetContent
,
SheetHeader
,
SheetFooter
,
SheetTitle
,
SheetDescription
};
web/src/components/ui/switch.tsx
0 → 100644
View file @
493832ae
import
*
as
SwitchPrimitive
from
"@radix-ui/react-switch"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Switch
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
SwitchPrimitive
.
Root
>
)
{
return
(
<
SwitchPrimitive
.
Root
data
-
slot=
"switch"
className=
{
cn
(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
,
className
,
)
}
{
...
props
}
>
<
SwitchPrimitive
.
Thumb
data
-
slot=
"switch-thumb"
className=
{
cn
(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
,
)
}
/>
</
SwitchPrimitive
.
Root
>
);
}
export
{
Switch
};
web/src/components/ui/textarea.tsx
0 → 100644
View file @
493832ae
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
Textarea
({
className
,
...
props
}:
React
.
ComponentProps
<
"textarea"
>
)
{
return
(
<
textarea
data
-
slot=
"textarea"
className=
{
cn
(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
,
className
,
)
}
{
...
props
}
/>
);
}
export
{
Textarea
};
web/src/components/ui/tooltip.tsx
0 → 100644
View file @
493832ae
import
*
as
TooltipPrimitive
from
"@radix-ui/react-tooltip"
;
import
*
as
React
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
function
TooltipProvider
({
delayDuration
=
0
,
...
props
}:
React
.
ComponentProps
<
typeof
TooltipPrimitive
.
Provider
>
)
{
return
<
TooltipPrimitive
.
Provider
data
-
slot=
"tooltip-provider"
delayDuration=
{
delayDuration
}
{
...
props
}
/>;
}
function
Tooltip
({
...
props
}:
React
.
ComponentProps
<
typeof
TooltipPrimitive
.
Root
>
)
{
return
(
<
TooltipProvider
>
<
TooltipPrimitive
.
Root
data
-
slot=
"tooltip"
{
...
props
}
/>
</
TooltipProvider
>
);
}
function
TooltipTrigger
({
...
props
}:
React
.
ComponentProps
<
typeof
TooltipPrimitive
.
Trigger
>
)
{
return
<
TooltipPrimitive
.
Trigger
data
-
slot=
"tooltip-trigger"
{
...
props
}
/>;
}
function
TooltipContent
({
className
,
sideOffset
=
0
,
children
,
...
props
}:
React
.
ComponentProps
<
typeof
TooltipPrimitive
.
Content
>
)
{
return
(
<
TooltipPrimitive
.
Portal
>
<
TooltipPrimitive
.
Content
data
-
slot=
"tooltip-content"
sideOffset=
{
sideOffset
}
className=
{
cn
(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance"
,
className
,
)
}
{
...
props
}
>
{
children
}
<
TooltipPrimitive
.
Arrow
className=
"bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]"
/>
</
TooltipPrimitive
.
Content
>
</
TooltipPrimitive
.
Portal
>
);
}
export
{
Tooltip
,
TooltipTrigger
,
TooltipContent
,
TooltipProvider
};
web/src/layouts/HomeLayout.tsx
View file @
493832ae
...
...
@@ -3,7 +3,7 @@ import { Outlet } from "react-router-dom";
import
{
HomeSidebar
,
HomeSidebarDrawer
}
from
"@/components/HomeSidebar"
;
import
MobileHeader
from
"@/components/MobileHeader"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
{
cn
}
from
"@/utils"
;
import
{
cn
}
from
"@/
lib/
utils"
;
const
HomeLayout
=
observer
(()
=>
{
const
{
md
,
lg
}
=
useResponsiveWidth
();
...
...
web/src/layouts/RootLayout.tsx
View file @
493832ae
...
...
@@ -5,11 +5,11 @@ import usePrevious from "react-use/lib/usePrevious";
import
Navigation
from
"@/components/Navigation"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
{
cn
}
from
"@/lib/utils"
;
import
Loading
from
"@/pages/Loading"
;
import
{
Routes
}
from
"@/router"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
memoFilterStore
from
"@/store/v2/memoFilter"
;
import
{
cn
}
from
"@/utils"
;
const
RootLayout
=
observer
(()
=>
{
const
location
=
useLocation
();
...
...
web/src/
utils
/utils.ts
→
web/src/
lib
/utils.ts
View file @
493832ae
import
{
type
ClassValue
,
clsx
}
from
"clsx"
;
import
{
clsx
,
type
ClassValue
}
from
"clsx"
;
import
{
twMerge
}
from
"tailwind-merge"
;
export
const
cn
=
(...
inputs
:
ClassValue
[])
=>
{
export
function
cn
(...
inputs
:
ClassValue
[])
{
return
twMerge
(
clsx
(
inputs
));
}
;
}
web/src/main.tsx
View file @
493832ae
import
"@github/relative-time-element"
;
import
{
CssVarsProvider
}
from
"@mui/joy"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
createRoot
}
from
"react-dom/client"
;
import
{
Toaster
}
from
"react-hot-toast"
;
...
...
@@ -9,15 +8,13 @@ import router from "./router";
import
{
initialUserStore
}
from
"./store/v2/user"
;
import
{
initialWorkspaceStore
}
from
"./store/v2/workspace"
;
import
"./style.css"
;
import
theme
from
"./theme"
;
import
"@usememos/mui/dist/index.css"
;
import
"leaflet/dist/leaflet.css"
;
const
Main
=
observer
(()
=>
(
<
CssVarsProvider
theme=
{
theme
}
>
<>
<
RouterProvider
router=
{
router
}
/>
<
Toaster
position=
"top-right"
toastOptions=
{
{
className
:
"dark:bg-zinc-700 dark:text-gray-300"
}
}
/>
</
CssVarsProvider
>
</>
));
(
async
()
=>
{
...
...
web/src/pages/Attachments.tsx
View file @
493832ae
import
{
Divider
,
Tooltip
}
from
"@mui/joy"
;
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
dayjs
from
"dayjs"
;
import
{
includes
}
from
"lodash-es"
;
import
{
PaperclipIcon
,
SearchIcon
,
TrashIcon
}
from
"lucide-react"
;
...
...
@@ -8,6 +6,10 @@ import { useEffect, useState } from "react";
import
AttachmentIcon
from
"@/components/AttachmentIcon"
;
import
Empty
from
"@/components/Empty"
;
import
MobileHeader
from
"@/components/MobileHeader"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
Tooltip
,
TooltipContent
,
TooltipProvider
,
TooltipTrigger
}
from
"@/components/ui/tooltip"
;
import
{
attachmentServiceClient
}
from
"@/grpcweb"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
...
...
@@ -75,13 +77,15 @@ const Attachments = observer(() => {
<
span
className=
"text-lg"
>
{
t
(
"common.attachments"
)
}
</
span
>
</
p
>
<
div
>
<
Input
className=
"max-w-32"
placeholder=
{
t
(
"common.search"
)
}
startDecorator=
{
<
SearchIcon
className=
"w-4 h-auto"
/>
}
value=
{
state
.
searchQuery
}
onChange=
{
(
e
)
=>
setState
({
...
state
,
searchQuery
:
e
.
target
.
value
})
}
/>
<
div
className=
"relative max-w-32"
>
<
SearchIcon
className=
"absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-500"
/>
<
Input
className=
"pl-9"
placeholder=
{
t
(
"common.search"
)
}
value=
{
state
.
searchQuery
}
onChange=
{
(
e
)
=>
setState
({
...
state
,
searchQuery
:
e
.
target
.
value
})
}
/>
</
div
>
</
div
>
</
div
>
<
div
className=
"w-full flex flex-col justify-start items-start mt-4 mb-6"
>
...
...
@@ -127,18 +131,25 @@ const Attachments = observer(() => {
{
unusedAttachments
.
length
>
0
&&
(
<>
<
Divide
r
/>
<
Separato
r
/>
<
div
className=
"w-full flex flex-row justify-start items-start"
>
<
div
className=
"w-16 sm:w-24 sm:pl-4 flex flex-col justify-start items-start"
></
div
>
<
div
className=
"w-full max-w-[calc(100%-4rem)] sm:max-w-[calc(100%-6rem)] flex flex-row justify-start items-start gap-4 flex-wrap"
>
<
div
className=
"w-full flex flex-row justify-start items-center gap-2"
>
<
span
className=
"text-gray-600 dark:text-gray-400"
>
{
t
(
"resource.unused-resources"
)
}
</
span
>
<
span
className=
"text-gray-500 dark:text-gray-500 opacity-80"
>
(
{
unusedAttachments
.
length
}
)
</
span
>
<
Tooltip
title=
"Delete all"
placement=
"top"
>
<
Button
variant=
"plain"
onClick=
{
handleDeleteUnusedAttachments
}
>
<
TrashIcon
className=
"w-4 h-auto opacity-60"
/>
</
Button
>
</
Tooltip
>
<
TooltipProvider
>
<
Tooltip
>
<
TooltipTrigger
asChild
>
<
Button
variant=
"ghost"
size=
"sm"
onClick=
{
handleDeleteUnusedAttachments
}
>
<
TrashIcon
className=
"w-4 h-auto opacity-60"
/>
</
Button
>
</
TooltipTrigger
>
<
TooltipContent
>
<
p
>
Delete all
</
p
>
</
TooltipContent
>
</
Tooltip
>
</
TooltipProvider
>
</
div
>
{
unusedAttachments
.
map
((
attachment
)
=>
{
return
(
...
...
web/src/pages/MemoDetail.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
{
ArrowUpLeftFromCircleIcon
,
MessageCircleIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
ClientError
}
from
"nice-grpc-web"
;
...
...
@@ -9,14 +8,15 @@ import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDet
import
MemoEditor
from
"@/components/MemoEditor"
;
import
MemoView
from
"@/components/MemoView"
;
import
MobileHeader
from
"@/components/MobileHeader"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
memoNamePrefix
}
from
"@/store/common"
;
import
{
memoStore
}
from
"@/store/v2"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
import
{
Memo
,
MemoRelation_Type
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
const
MemoDetail
=
observer
(()
=>
{
...
...
@@ -122,7 +122,7 @@ const MemoDetail = observer(() => {
{
comments
.
length
===
0
?
(
showCreateCommentButton
&&
(
<
div
className=
"w-full flex flex-row justify-center items-center py-6"
>
<
Button
variant=
"
plain"
color=
"primary
"
onClick=
{
handleShowCommentEditor
}
>
<
Button
variant=
"
ghost
"
onClick=
{
handleShowCommentEditor
}
>
<
span
className=
"text-gray-500"
>
{
t
(
"memo.comment.write-a-comment"
)
}
</
span
>
<
MessageCircleIcon
className=
"ml-2 w-5 h-auto text-gray-500"
/>
</
Button
>
...
...
@@ -137,7 +137,7 @@ const MemoDetail = observer(() => {
<
span
className=
"text-gray-400 text-sm ml-1"
>
(
{
comments
.
length
}
)
</
span
>
</
div
>
{
showCreateCommentButton
&&
(
<
Button
variant=
"
plain"
color=
"primary
"
className=
"text-gray-500"
onClick=
{
handleShowCommentEditor
}
>
<
Button
variant=
"
ghost
"
className=
"text-gray-500"
onClick=
{
handleShowCommentEditor
}
>
{
t
(
"memo.comment.write-a-comment"
)
}
</
Button
>
)
}
...
...
web/src/pages/Setting.tsx
View file @
493832ae
import
{
Option
,
Select
}
from
"@mui/joy"
;
import
{
CogIcon
,
DatabaseIcon
,
KeyIcon
,
LibraryIcon
,
LucideIcon
,
Settings2Icon
,
UserIcon
,
UsersIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useCallback
,
useEffect
,
useMemo
,
useState
}
from
"react"
;
...
...
@@ -12,6 +11,7 @@ import SSOSection from "@/components/Settings/SSOSection";
import
SectionMenuItem
from
"@/components/Settings/SectionMenuItem"
;
import
StorageSection
from
"@/components/Settings/StorageSection"
;
import
WorkspaceSection
from
"@/components/Settings/WorkspaceSection"
;
import
{
Select
,
SelectContent
,
SelectItem
,
SelectTrigger
,
SelectValue
}
from
"@/components/ui/select"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
{
workspaceStore
}
from
"@/store/v2"
;
...
...
@@ -123,12 +123,17 @@ const Setting = observer(() => {
</
div
>
<
div
className=
"w-full grow sm:pl-4 overflow-x-auto"
>
<
div
className=
"w-auto inline-block my-2 sm:hidden"
>
<
Select
value=
{
state
.
selectedSection
}
onChange=
{
(
_
,
value
)
=>
handleSectionSelectorItemClick
(
value
as
SettingSection
)
}
>
{
settingsSectionList
.
map
((
settingSection
)
=>
(
<
Option
key=
{
settingSection
}
value=
{
settingSection
}
>
{
t
(
`setting.${settingSection}`
)
}
</
Option
>
))
}
<
Select
value=
{
state
.
selectedSection
}
onValueChange=
{
(
value
)
=>
handleSectionSelectorItemClick
(
value
as
SettingSection
)
}
>
<
SelectTrigger
className=
"w-[180px]"
>
<
SelectValue
placeholder=
"Select section"
/>
</
SelectTrigger
>
<
SelectContent
>
{
settingsSectionList
.
map
((
settingSection
)
=>
(
<
SelectItem
key=
{
settingSection
}
value=
{
settingSection
}
>
{
t
(
`setting.${settingSection}`
)
}
</
SelectItem
>
))
}
</
SelectContent
>
</
Select
>
</
div
>
{
state
.
selectedSection
===
"my-account"
?
(
...
...
web/src/pages/SignIn.tsx
View file @
493832ae
import
{
Divider
}
from
"@mui/joy"
;
import
{
Button
}
from
"@usememos/mui"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
toast
}
from
"react-hot-toast"
;
import
{
Link
}
from
"react-router-dom"
;
import
AuthFooter
from
"@/components/AuthFooter"
;
import
PasswordSignInForm
from
"@/components/PasswordSignInForm"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Separator
}
from
"@/components/ui/separator"
;
import
{
identityProviderServiceClient
}
from
"@/grpcweb"
;
import
{
absolutifyLink
}
from
"@/helpers/utils"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
...
...
@@ -79,14 +79,20 @@ const SignIn = observer(() => {
)
}
{
identityProviderList
.
length
>
0
&&
(
<>
{
!
workspaceGeneralSetting
.
disallowPasswordAuth
&&
<
Divider
className=
"my-4!"
>
{
t
(
"common.or"
)
}
</
Divider
>
}
{
!
workspaceGeneralSetting
.
disallowPasswordAuth
&&
(
<
div
className=
"relative my-4 w-full"
>
<
Separator
/>
<
div
className=
"absolute inset-0 flex items-center justify-center"
>
<
span
className=
"bg-background px-2 text-xs text-muted-foreground"
>
{
t
(
"common.or"
)
}
</
span
>
</
div
>
</
div
>
)
}
<
div
className=
"w-full flex flex-col space-y-2"
>
{
identityProviderList
.
map
((
identityProvider
)
=>
(
<
Button
className=
"bg-white dark:bg-black"
className=
"bg-white dark:bg-black
w-full
"
key=
{
identityProvider
.
name
}
variant=
"outlined"
fullWidth
variant=
"outline"
onClick=
{
()
=>
handleSignInWithIdentityProvider
(
identityProvider
)
}
>
{
t
(
"common.sign-in-with"
,
{
provider
:
identityProvider
.
title
})
}
...
...
web/src/pages/SignUp.tsx
View file @
493832ae
import
{
Button
,
Input
}
from
"@usememos/mui"
;
import
{
LoaderIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
ClientError
}
from
"nice-grpc-web"
;
...
...
@@ -6,6 +5,8 @@ import { useState } from "react";
import
{
toast
}
from
"react-hot-toast"
;
import
{
Link
}
from
"react-router-dom"
;
import
AuthFooter
from
"@/components/AuthFooter"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
authServiceClient
,
userServiceClient
}
from
"@/grpcweb"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
useNavigateTo
from
"@/hooks/useNavigateTo"
;
...
...
@@ -83,8 +84,7 @@ const SignUp = observer(() => {
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
span
className=
"leading-8 text-gray-600"
>
{
t
(
"common.username"
)
}
</
span
>
<
Input
className=
"w-full bg-white dark:bg-black"
size=
"lg"
className=
"w-full bg-white dark:bg-black h-10"
type=
"text"
readOnly=
{
actionBtnLoadingState
.
isLoading
}
placeholder=
{
t
(
"common.username"
)
}
...
...
@@ -99,8 +99,7 @@ const SignUp = observer(() => {
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
span
className=
"leading-8 text-gray-600"
>
{
t
(
"common.password"
)
}
</
span
>
<
Input
className=
"w-full bg-white dark:bg-black"
size=
"lg"
className=
"w-full bg-white dark:bg-black h-10"
type=
"password"
readOnly=
{
actionBtnLoadingState
.
isLoading
}
placeholder=
{
t
(
"common.password"
)
}
...
...
@@ -114,14 +113,7 @@ const SignUp = observer(() => {
</
div
>
</
div
>
<
div
className=
"flex flex-row justify-end items-center w-full mt-6"
>
<
Button
type=
"submit"
color=
"primary"
size=
"lg"
fullWidth
disabled=
{
actionBtnLoadingState
.
isLoading
}
onClick=
{
handleSignUpButtonClick
}
>
<
Button
type=
"submit"
className=
"w-full h-10"
disabled=
{
actionBtnLoadingState
.
isLoading
}
onClick=
{
handleSignUpButtonClick
}
>
{
t
(
"common.sign-up"
)
}
{
actionBtnLoadingState
.
isLoading
&&
<
LoaderIcon
className=
"w-5 h-auto ml-2 animate-spin opacity-60"
/>
}
</
Button
>
...
...
web/src/pages/UserProfile.tsx
View file @
493832ae
import
{
Button
}
from
"@usememos/mui"
;
import
copy
from
"copy-to-clipboard"
;
import
dayjs
from
"dayjs"
;
import
{
ExternalLinkIcon
}
from
"lucide-react"
;
...
...
@@ -9,6 +8,7 @@ import { useParams } from "react-router-dom";
import
MemoView
from
"@/components/MemoView"
;
import
PagedMemoList
from
"@/components/PagedMemoList"
;
import
UserAvatar
from
"@/components/UserAvatar"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
{
viewStore
,
userStore
}
from
"@/store/v2"
;
import
memoFilterStore
from
"@/store/v2/memoFilter"
;
...
...
@@ -81,7 +81,7 @@ const UserProfile = observer(() => {
(
user
?
(
<>
<
div
className=
"my-4 w-full flex justify-end items-center gap-2"
>
<
Button
variant=
"outline
d
"
onClick=
{
handleCopyProfileLink
}
>
<
Button
variant=
"outline"
onClick=
{
handleCopyProfileLink
}
>
{
t
(
"common.share"
)
}
<
ExternalLinkIcon
className=
"ml-1 w-4 h-auto opacity-60"
/>
</
Button
>
...
...
web/src/style.css
View file @
493832ae
@import
"tailwindcss"
;
@import
"tw-animate-css"
;
@custom-variant
dark
(
&
:
is
(.
dark
*
));
@theme
{
--default-transition-duration
:
150ms
;
}
@variant
dark
(
&
:
is
(.
dark
*
));
@theme
inline
{
--radius-sm
:
calc
(
var
(
--radius
)
-
4px
);
--radius-md
:
calc
(
var
(
--radius
)
-
2px
);
--radius-lg
:
var
(
--radius
);
--radius-xl
:
calc
(
var
(
--radius
)
+
4px
);
--color-background
:
var
(
--background
);
--color-foreground
:
var
(
--foreground
);
--color-card
:
var
(
--card
);
--color-card-foreground
:
var
(
--card-foreground
);
--color-popover
:
var
(
--popover
);
--color-popover-foreground
:
var
(
--popover-foreground
);
--color-primary
:
var
(
--primary
);
--color-primary-foreground
:
var
(
--primary-foreground
);
--color-secondary
:
var
(
--secondary
);
--color-secondary-foreground
:
var
(
--secondary-foreground
);
--color-muted
:
var
(
--muted
);
--color-muted-foreground
:
var
(
--muted-foreground
);
--color-accent
:
var
(
--accent
);
--color-accent-foreground
:
var
(
--accent-foreground
);
--color-destructive
:
var
(
--destructive
);
--color-border
:
var
(
--border
);
--color-input
:
var
(
--input
);
--color-ring
:
var
(
--ring
);
--color-chart-1
:
var
(
--chart-1
);
--color-chart-2
:
var
(
--chart-2
);
--color-chart-3
:
var
(
--chart-3
);
--color-chart-4
:
var
(
--chart-4
);
--color-chart-5
:
var
(
--chart-5
);
--color-sidebar
:
var
(
--sidebar
);
--color-sidebar-foreground
:
var
(
--sidebar-foreground
);
--color-sidebar-primary
:
var
(
--sidebar-primary
);
--color-sidebar-primary-foreground
:
var
(
--sidebar-primary-foreground
);
--color-sidebar-accent
:
var
(
--sidebar-accent
);
--color-sidebar-accent-foreground
:
var
(
--sidebar-accent-foreground
);
--color-sidebar-border
:
var
(
--sidebar-border
);
--color-sidebar-ring
:
var
(
--sidebar-ring
);
}
:root
{
--radius
:
0.625rem
;
--background
:
oklch
(
1
0
0
);
--foreground
:
oklch
(
0.141
0.005
285.823
);
--card
:
oklch
(
1
0
0
);
--card-foreground
:
oklch
(
0.141
0.005
285.823
);
--popover
:
oklch
(
1
0
0
);
--popover-foreground
:
oklch
(
0.141
0.005
285.823
);
--primary
:
oklch
(
0.21
0.006
285.885
);
--primary-foreground
:
oklch
(
0.985
0
0
);
--secondary
:
oklch
(
0.967
0.001
286.375
);
--secondary-foreground
:
oklch
(
0.21
0.006
285.885
);
--muted
:
oklch
(
0.967
0.001
286.375
);
--muted-foreground
:
oklch
(
0.552
0.016
285.938
);
--accent
:
oklch
(
0.967
0.001
286.375
);
--accent-foreground
:
oklch
(
0.21
0.006
285.885
);
--destructive
:
oklch
(
0.577
0.245
27.325
);
--border
:
oklch
(
0.92
0.004
286.32
);
--input
:
oklch
(
0.92
0.004
286.32
);
--ring
:
oklch
(
0.705
0.015
286.067
);
--chart-1
:
oklch
(
0.646
0.222
41.116
);
--chart-2
:
oklch
(
0.6
0.118
184.704
);
--chart-3
:
oklch
(
0.398
0.07
227.392
);
--chart-4
:
oklch
(
0.828
0.189
84.429
);
--chart-5
:
oklch
(
0.769
0.188
70.08
);
--sidebar
:
oklch
(
0.985
0
0
);
--sidebar-foreground
:
oklch
(
0.141
0.005
285.823
);
--sidebar-primary
:
oklch
(
0.21
0.006
285.885
);
--sidebar-primary-foreground
:
oklch
(
0.985
0
0
);
--sidebar-accent
:
oklch
(
0.967
0.001
286.375
);
--sidebar-accent-foreground
:
oklch
(
0.21
0.006
285.885
);
--sidebar-border
:
oklch
(
0.92
0.004
286.32
);
--sidebar-ring
:
oklch
(
0.705
0.015
286.067
);
}
.dark
{
--background
:
oklch
(
0.141
0.005
285.823
);
--foreground
:
oklch
(
0.985
0
0
);
--card
:
oklch
(
0.21
0.006
285.885
);
--card-foreground
:
oklch
(
0.985
0
0
);
--popover
:
oklch
(
0.21
0.006
285.885
);
--popover-foreground
:
oklch
(
0.985
0
0
);
--primary
:
oklch
(
0.92
0.004
286.32
);
--primary-foreground
:
oklch
(
0.21
0.006
285.885
);
--secondary
:
oklch
(
0.274
0.006
286.033
);
--secondary-foreground
:
oklch
(
0.985
0
0
);
--muted
:
oklch
(
0.274
0.006
286.033
);
--muted-foreground
:
oklch
(
0.705
0.015
286.067
);
--accent
:
oklch
(
0.274
0.006
286.033
);
--accent-foreground
:
oklch
(
0.985
0
0
);
--destructive
:
oklch
(
0.704
0.191
22.216
);
--border
:
oklch
(
1
0
0
/
10%
);
--input
:
oklch
(
1
0
0
/
15%
);
--ring
:
oklch
(
0.552
0.016
285.938
);
--chart-1
:
oklch
(
0.488
0.243
264.376
);
--chart-2
:
oklch
(
0.696
0.17
162.48
);
--chart-3
:
oklch
(
0.769
0.188
70.08
);
--chart-4
:
oklch
(
0.627
0.265
303.9
);
--chart-5
:
oklch
(
0.645
0.246
16.439
);
--sidebar
:
oklch
(
0.21
0.006
285.885
);
--sidebar-foreground
:
oklch
(
0.985
0
0
);
--sidebar-primary
:
oklch
(
0.488
0.243
264.376
);
--sidebar-primary-foreground
:
oklch
(
0.985
0
0
);
--sidebar-accent
:
oklch
(
0.274
0.006
286.033
);
--sidebar-accent-foreground
:
oklch
(
0.985
0
0
);
--sidebar-border
:
oklch
(
1
0
0
/
10%
);
--sidebar-ring
:
oklch
(
0.552
0.016
285.938
);
}
@layer
base
{
*
{
@apply
border-border
outline-ring/50;
}
body
{
@apply
bg-background
text-foreground;
}
}
web/src/theme/index.ts
View file @
493832ae
import
{
extendTheme
}
from
"@mui/joy"
;
// Theme configuration for Tailwind CSS v4
// This file is kept for compatibility but no longer used for MUI theme configuration
// All styling is now handled by Tailwind CSS
const
theme
=
extendTheme
({
components
:
{
JoyButton
:
{
defaultProps
:
{
size
:
"sm"
,
},
},
JoyInput
:
{
defaultProps
:
{
size
:
"sm"
,
},
},
JoySelect
:
{
defaultProps
:
{
size
:
"sm"
,
},
styleOverrides
:
{
listbox
:
{
zIndex
:
9999
,
},
},
},
JoyAutocomplete
:
{
styleOverrides
:
{
listbox
:
{
zIndex
:
9999
,
},
},
},
},
});
export
default
theme
;
export
default
{};
web/src/utils/index.ts
deleted
100644 → 0
View file @
50a41a39
export
*
from
"./utils"
;
web/vite.config.mts
View file @
493832ae
...
...
@@ -46,7 +46,6 @@ export default defineConfig({
rollupOptions: {
output: {
manualChunks: {
"mui-vendor": ["@mui/joy", "@emotion/react", "@emotion/styled"],
"utils-vendor": ["dayjs", "lodash-es"],
"katex-vendor": ["katex"],
"mermaid-vendor": ["mermaid"],
...
...
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