mirror of
https://github.com/usememos/memos.git
synced 2025-11-23 11:02:23 +08:00
refactor: unify components
This commit is contained in:
parent
50a41a39a6
commit
493832aeb4
112 changed files with 2764 additions and 1654 deletions
21
web/components.json
Normal file
21
web/components.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
1870
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { cn } from "@/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cn } from "@/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
avatarUrl?: string;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
35
web/src/components/ui/badge.tsx
Normal file
35
web/src/components/ui/badge.tsx
Normal 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 };
|
||||
47
web/src/components/ui/button.tsx
Normal file
47
web/src/components/ui/button.tsx
Normal 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 };
|
||||
23
web/src/components/ui/checkbox.tsx
Normal file
23
web/src/components/ui/checkbox.tsx
Normal 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 };
|
||||
111
web/src/components/ui/command.tsx
Normal file
111
web/src/components/ui/command.tsx
Normal 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 };
|
||||
98
web/src/components/ui/dialog.tsx
Normal file
98
web/src/components/ui/dialog.tsx
Normal 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,
|
||||
};
|
||||
20
web/src/components/ui/input.tsx
Normal file
20
web/src/components/ui/input.tsx
Normal 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 };
|
||||
18
web/src/components/ui/label.tsx
Normal file
18
web/src/components/ui/label.tsx
Normal 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 };
|
||||
27
web/src/components/ui/radio-group.tsx
Normal file
27
web/src/components/ui/radio-group.tsx
Normal 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 };
|
||||
144
web/src/components/ui/select.tsx
Normal file
144
web/src/components/ui/select.tsx
Normal 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,
|
||||
};
|
||||
25
web/src/components/ui/separator.tsx
Normal file
25
web/src/components/ui/separator.tsx
Normal 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 };
|
||||
87
web/src/components/ui/sheet.tsx
Normal file
87
web/src/components/ui/sheet.tsx
Normal 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 };
|
||||
25
web/src/components/ui/switch.tsx
Normal file
25
web/src/components/ui/switch.tsx
Normal 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 };
|
||||
17
web/src/components/ui/textarea.tsx
Normal file
17
web/src/components/ui/textarea.tsx
Normal 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 };
|
||||
40
web/src/components/ui/tooltip.tsx
Normal file
40
web/src/components/ui/tooltip.tsx
Normal 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 };
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
6
web/src/lib/utils.ts
Normal 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
Loading…
Add table
Reference in a new issue