refactor: unify components

This commit is contained in:
Johnny 2025-07-01 19:29:48 +08:00
parent 50a41a39a6
commit 493832aeb4
112 changed files with 2764 additions and 1654 deletions

21
web/components.json Normal file
View file

@ -0,0 +1,21 @@
{
"$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"
}

View file

@ -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"

1870
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
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;

View file

@ -1,10 +1,10 @@
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>
);
},
);

View file

@ -1,6 +1,6 @@
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>
);
};

View file

@ -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";

View file

@ -1,6 +1,6 @@
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";

View file

@ -1,6 +1,6 @@
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 {

View file

@ -1,7 +1,8 @@
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}>

View file

@ -1,8 +1,10 @@
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>

View file

@ -1,8 +1,10 @@
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>

View file

@ -1,7 +1,9 @@
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}>

View file

@ -1,8 +1,10 @@
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}>

View file

@ -1,7 +1,8 @@
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}>

View file

@ -1,6 +1,6 @@
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";

View file

@ -1,10 +1,8 @@
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 />);

View file

@ -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";

View file

@ -1,8 +1,8 @@
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>
);
};

View file

@ -1,15 +1,15 @@
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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)$/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) => {

View file

@ -1,16 +1,16 @@
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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
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>

View file

@ -1,17 +1,17 @@
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>

View file

@ -1,5 +1,5 @@
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>
);
};

View file

@ -1,6 +1,6 @@
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>
);
};

View file

@ -1,6 +1,6 @@
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[];

View file

@ -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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
interface Props {
memo: Memo;

View file

@ -1,6 +1,6 @@
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";

View file

@ -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";

View file

@ -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 {

View file

@ -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";

View file

@ -1,4 +1,4 @@
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 <Divider className="my-3!" />;
return <Separator className="my-3!" />;
};
export default HorizontalRule;

View file

@ -1,5 +1,5 @@
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>
);
};

View file

@ -1,7 +1,7 @@
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 {

View file

@ -1,5 +1,5 @@
import TeX from "@matejmazur/react-katex";
import { cn } from "@/utils";
import { cn } from "@/lib/utils";
import "katex/dist/katex.min.css";
interface Props {

View file

@ -1,14 +1,32 @@
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 () => {

View file

@ -1,5 +1,5 @@
import { useState } from "react";
import { cn } from "@/utils";
import { cn } from "@/lib/utils";
interface Props {
content: string;

View file

@ -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 {

View file

@ -1,10 +1,10 @@
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) => (

View file

@ -1,9 +1,9 @@
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";

View file

@ -1,7 +1,7 @@
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";

View file

@ -1,8 +1,8 @@
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>
);
};

View file

@ -1,10 +1,10 @@
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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
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}
onChange={(_, value) =>
value={viewStore.state.orderByTimeAsc.toString()}
onValueChange={(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}
onChange={(_, value) =>
onValueChange={(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>

View file

@ -1,11 +1,13 @@
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>
</Chip>
))
}
onChange={(_, value) => setSelectedMemos(value)}
/>
</CommandItem>
))}
</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>

View file

@ -1,10 +1,11 @@
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"

View file

@ -1,8 +1,7 @@
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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover";
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>

View file

@ -1,10 +1,10 @@
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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover";
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}>

View file

@ -1,8 +1,8 @@
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"

View file

@ -1,9 +1,9 @@
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 {

View file

@ -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 = {

View file

@ -1,8 +1,8 @@
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 {

View file

@ -1,4 +1,3 @@
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>
)}

View file

@ -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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
interface Props {
location: Location;

View file

@ -1,10 +1,9 @@
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());

View file

@ -1,9 +1,9 @@
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 {

View file

@ -1,15 +1,15 @@
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">

View file

@ -1,6 +1,6 @@
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 {

View file

@ -1,12 +1,12 @@
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
)}

View file

@ -1,8 +1,8 @@
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>
</Button>
<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}
</span>
</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>
);
});

View file

@ -1,9 +1,9 @@
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>

View file

@ -1,9 +1,10 @@
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>

View file

@ -1,6 +1,6 @@
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;

View file

@ -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;

View file

@ -1,12 +1,12 @@
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>
);
});

View file

@ -1,8 +1,8 @@
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>

View file

@ -1,8 +1,8 @@
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";

View file

@ -1,8 +1,8 @@
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);
}}

View file

@ -1,10 +1,12 @@
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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
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">

View file

@ -1,10 +1,12 @@
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}
onChange={(event) => updatePartialSetting({ disallowPublicVisibility: event.target.checked })}
onCheckedChange={(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}
onChange={(event) => updatePartialSetting({ displayWithUpdateTime: event.target.checked })}
onCheckedChange={(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}
onChange={(event) => updatePartialSetting({ enableLinkPreview: event.target.checked })}
onCheckedChange={(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}
onChange={(event) => updatePartialSetting({ enableComment: event.target.checked })}
onCheckedChange={(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}
onChange={(event) => updatePartialSetting({ enableDoubleClickEdit: event.target.checked })}
onCheckedChange={(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}
onChange={(event) => updatePartialSetting({ disableMarkdownShortcuts: event.target.checked })}
onCheckedChange={(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}
onChange={(event) => updatePartialSetting({ enableBlurNsfwContent: event.target.checked })}
onCheckedChange={(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>

View file

@ -1,11 +1,11 @@
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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
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="outlined" 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="outlined">
<Button variant="outline">
<MoreVerticalIcon className="w-4 h-4 mx-auto" />
</Button>
</PopoverTrigger>

View file

@ -1,5 +1,6 @@
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>

View file

@ -1,15 +1,15 @@
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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
const SSOSection = () => {
const t = useTranslate();
@ -48,7 +48,7 @@ const SSOSection = () => {
{t("common.create")}
</Button>
</div>
<Divider />
<Separator />
{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>
);

View file

@ -1,11 +1,16 @@
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>
);

View file

@ -1,7 +1,7 @@
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);

View file

@ -1,7 +1,7 @@
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);
}}

View file

@ -1,11 +1,14 @@
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="outlined" onClick={handleUpdateCustomizedProfileButtonClick}>
<Button variant="outline" onClick={handleUpdateCustomizedProfileButtonClick}>
{t("common.edit")}
</Button>
</div>
<Divider />
<Separator />
<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}
onChange={(event) => updatePartialSetting({ disallowUserRegistration: event.target.checked })}
onCheckedChange={(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}
onChange={(event) => updatePartialSetting({ disallowPasswordAuth: event.target.checked })}
onCheckedChange={(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}
onChange={(event) => updatePartialSetting({ disallowChangeUsername: event.target.checked })}
onCheckedChange={(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}
onChange={(event) => updatePartialSetting({ disallowChangeNickname: event.target.checked })}
onCheckedChange={(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>

View file

@ -1,6 +1,6 @@
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>
);
}

View file

@ -1,8 +1,10 @@
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>

View file

@ -1,7 +1,9 @@
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="outlined" 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}>

View file

@ -1,4 +1,4 @@
import { cn } from "@/utils";
import { cn } from "@/lib/utils";
interface Props {
avatarUrl?: string;

View file

@ -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/Popover";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
interface Props {
collapsed?: boolean;

View file

@ -1,6 +1,6 @@
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;

View file

@ -1,6 +1,6 @@
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>
);
};

View file

@ -1,28 +1,34 @@
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 };

View file

@ -0,0 +1,35 @@
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 };

View file

@ -0,0 +1,47 @@
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 };

View file

@ -0,0 +1,23 @@
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 };

View file

@ -0,0 +1,111 @@
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 };

View file

@ -0,0 +1,98 @@
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,
};

View file

@ -0,0 +1,20 @@
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 };

View file

@ -0,0 +1,18 @@
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 };

View file

@ -0,0 +1,27 @@
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 };

View file

@ -0,0 +1,144 @@
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,
};

View file

@ -0,0 +1,25 @@
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 };

View file

@ -0,0 +1,87 @@
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 };

View file

@ -0,0 +1,25 @@
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 };

View file

@ -0,0 +1,17 @@
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 };

View file

@ -0,0 +1,40 @@
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 };

View file

@ -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();

View file

@ -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();

6
web/src/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

Some files were not shown because too many files have changed in this diff Show more