UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: редизайн окон настроек workspace и profile

This commit is contained in:
DCCONSTRUCTIONS 2026-04-22 17:22:33 +03:00
parent 290b00d251
commit 85bd24c45b
27 changed files with 256 additions and 198 deletions

View File

@ -113,7 +113,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
"opacity-60": !canPerformWorkspaceMemberActions, "opacity-60": !canPerformWorkspaceMemberActions,
})} })}
> >
<div className="flex items-center justify-between gap-4 pb-3.5"> <div className="flex items-center justify-between gap-4 pb-4">
<h4 className="flex items-center gap-2.5 text-h3-medium"> <h4 className="flex items-center gap-2.5 text-h3-medium">
{t("workspace_settings.settings.members.title")} {t("workspace_settings.settings.members.title")}
{workspaceMemberIds && workspaceMemberIds.length > 0 && ( {workspaceMemberIds && workspaceMemberIds.length > 0 && (
@ -121,7 +121,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
)} )}
</h4> </h4>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 rounded-md border border-subtle bg-surface-1 px-2.5 py-1.5"> <div className="nodedc-settings-field flex min-h-[2.75rem] items-center gap-1.5 px-3.5">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" /> <SearchIcon className="h-3.5 w-3.5 text-placeholder" />
<input <input
className="w-full max-w-[234px] border-none bg-transparent text-body-xs-regular outline-none placeholder:text-placeholder" className="w-full max-w-[234px] border-none bg-transparent text-body-xs-regular outline-none placeholder:text-placeholder"
@ -139,7 +139,12 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
/> />
<MembersActivityButton workspaceSlug={workspaceSlug} /> <MembersActivityButton workspaceSlug={workspaceSlug} />
{canPerformWorkspaceAdminActions && ( {canPerformWorkspaceAdminActions && (
<Button variant="primary" size="lg" onClick={() => setInviteModal(true)}> <Button
variant="ghost"
size="lg"
className="nodedc-settings-primary-button min-w-[11rem]"
onClick={() => setInviteModal(true)}
>
{t("workspace_settings.settings.members.add_member")} {t("workspace_settings.settings.members.add_member")}
</Button> </Button>
)} )}

View File

@ -12,13 +12,13 @@ import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
// components // components
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view"; import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title"; import { PageHead } from "@/components/core/page-title";
import { SettingsHeading } from "@/components/settings/heading"; import { SettingsHeading } from "@/components/settings/heading";
import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook"; import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper"; import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks"; import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks";
import { WebhooksEmptyState } from "@/components/web-hooks/empty-state";
// hooks // hooks
import { useWebhook } from "@/hooks/store/use-webhook"; import { useWebhook } from "@/hooks/store/use-webhook";
import { useWorkspace } from "@/hooks/store/use-workspace"; import { useWorkspace } from "@/hooks/store/use-workspace";
@ -78,7 +78,12 @@ function WebhooksListPage({ params }: Route.ComponentProps) {
title={t("workspace_settings.settings.webhooks.title")} title={t("workspace_settings.settings.webhooks.title")}
description={t("workspace_settings.settings.webhooks.description")} description={t("workspace_settings.settings.webhooks.description")}
control={ control={
<Button variant="primary" size="lg" onClick={() => setShowCreateWebhookModal(true)}> <Button
variant="ghost"
size="lg"
className="nodedc-settings-primary-button min-w-[11rem]"
onClick={() => setShowCreateWebhookModal(true)}
>
{t("workspace_settings.settings.webhooks.add_webhook")} {t("workspace_settings.settings.webhooks.add_webhook")}
</Button> </Button>
} }
@ -90,21 +95,9 @@ function WebhooksListPage({ params }: Route.ComponentProps) {
) : ( ) : (
<div className="flex h-full w-full flex-col"> <div className="flex h-full w-full flex-col">
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<EmptyStateCompact <div className="py-20">
assetKey="webhook" <WebhooksEmptyState onClick={() => setShowCreateWebhookModal(true)} />
title={t("settings_empty_state.webhooks.title")} </div>
description={t("settings_empty_state.webhooks.description")}
actions={[
{
label: t("settings_empty_state.webhooks.cta_primary"),
onClick: () => {
setShowCreateWebhookModal(true);
},
},
]}
align="start"
rootClassName="py-20"
/>
</div> </div>
</div> </div>
)} )}

View File

@ -39,7 +39,8 @@ export const DeleteWorkspaceSection = observer(function DeleteWorkspaceSection(p
description={t("workspace_settings.settings.general.delete_workspace_description")} description={t("workspace_settings.settings.general.delete_workspace_description")}
control={ control={
<Button <Button
variant="error-outline" variant="ghost"
className="nodedc-modal-danger-button min-w-[10rem]"
onClick={() => setDeleteWorkspaceModal(true)} onClick={() => setDeleteWorkspaceModal(true)}
data-ph-element={WORKSPACE_TRACKER_ELEMENTS.DELETE_WORKSPACE_BUTTON} data-ph-element={WORKSPACE_TRACKER_ELEMENTS.DELETE_WORKSPACE_BUTTON}
> >

View File

@ -30,31 +30,31 @@ export function ApiTokenListItem(props: Props) {
return ( return (
<> <>
<DeleteApiTokenModal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} tokenId={token.id} /> <DeleteApiTokenModal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} tokenId={token.id} />
<div className="group relative flex flex-col justify-center border-b border-subtle py-3"> <div className="nodedc-settings-card group relative flex flex-col justify-center px-5 py-4">
<Tooltip tooltipContent="Delete token" isMobile={isMobile}> <Tooltip tooltipContent="Delete token" isMobile={isMobile}>
<button <button
onClick={() => setDeleteModalOpen(true)} onClick={() => setDeleteModalOpen(true)}
className="absolute right-4 hidden place-items-center group-hover:grid" className="nodedc-settings-chip absolute top-4 right-4 hidden min-h-[2.25rem] place-items-center !px-2.5 group-hover:grid"
data-ph-element={PROFILE_SETTINGS_TRACKER_ELEMENTS.LIST_ITEM_DELETE_ICON} data-ph-element={PROFILE_SETTINGS_TRACKER_ELEMENTS.LIST_ITEM_DELETE_ICON}
> >
<XCircle className="h-4 w-4 text-danger-primary" /> <XCircle className="h-4 w-4 text-danger-primary" />
</button> </button>
</Tooltip> </Tooltip>
<div className="flex w-4/5 items-center"> <div className="flex w-4/5 items-center">
<h5 className="truncate text-13 font-medium">{token.label}</h5> <h5 className="truncate text-13 font-medium text-primary">{token.label}</h5>
<span <span
className={`${ className={`${
token.is_active ? "bg-success-subtle text-success-primary" : "bg-layer-1 text-placeholder" token.is_active ? "bg-success-subtle text-success-primary" : "bg-white/6 text-placeholder"
} ml-2 flex h-4 max-h-fit items-center rounded-xs px-2 text-11 font-medium`} } ml-2 flex min-h-[1.35rem] max-h-fit items-center rounded-full px-2.5 text-[11px] font-medium`}
> >
{token.is_active ? "Active" : "Expired"} {token.is_active ? "Active" : "Expired"}
</span> </span>
</div> </div>
<div className="mt-1 flex w-full flex-col justify-center"> <div className="mt-1 flex w-full flex-col justify-center">
{token.description.trim() !== "" && ( {token.description.trim() !== "" && (
<p className="mb-1 max-w-[70%] text-13 break-words">{token.description}</p> <p className="mb-1 max-w-[70%] text-13 break-words text-secondary">{token.description}</p>
)} )}
<p className="mb-1 text-11 leading-6 text-placeholder"> <p className="mb-1 text-11 leading-6 text-tertiary">
{token.is_active {token.is_active
? token.expired_at ? token.expired_at
? `Expires ${renderFormattedDate(token.expired_at)} at ${renderFormattedTime(token.expired_at)}` ? `Expires ${renderFormattedDate(token.expired_at)} at ${renderFormattedTime(token.expired_at)}`

View File

@ -21,9 +21,10 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
// import { Tooltip } from "@plane/propel/tooltip"; // import { Tooltip } from "@plane/propel/tooltip";
// import { EIssuesStoreType } from "@plane/types"; // import { EIssuesStoreType } from "@plane/types";
import type { TWorkItemFilterExpression } from "@plane/types"; import type { TWorkItemFilterExpression } from "@plane/types";
import { CustomSelect, SearchSelectionDropdown } from "@plane/ui"; import { SearchSelectionDropdown } from "@plane/ui";
// import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level"; // import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
// import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row"; // import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useUser, useUserPermissions } from "@/hooks/store/user"; import { useUser, useUserPermissions } from "@/hooks/store/user";
import { ProjectExportService } from "@/services/project/project-export.service"; import { ProjectExportService } from "@/services/project/project-export.service";
@ -171,6 +172,8 @@ export const ExportForm = observer(function ExportForm(props: Props) {
.join(", ") .join(", ")
: "All projects" : "All projects"
} }
className="!rounded-[1.25rem]"
buttonClassName="nodedc-settings-select !border-0 !px-4 !py-3 text-13 font-medium"
optionsClassName="max-w-48 sm:max-w-[532px]" optionsClassName="max-w-48 sm:max-w-[532px]"
placement="bottom-end" placement="bottom-end"
multiple multiple
@ -189,26 +192,30 @@ export const ExportForm = observer(function ExportForm(props: Props) {
name="provider" name="provider"
disabled={!isMember && (!hasProjects || !canPerformAnyCreateAction)} disabled={!isMember && (!hasProjects || !canPerformAnyCreateAction)}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<CustomSelect <SelectionDropdown
value={value} menuButton={t(value.i18n_title)}
onChange={onChange} menuButtonWrapperClassName="nodedc-settings-select px-4 py-3 text-13 font-medium"
label={t(value.i18n_title)} dropdownContentClassName="max-w-48 sm:max-w-[532px]"
optionsClassName="max-w-48 sm:max-w-[532px]"
placement="bottom-end" placement="bottom-end"
buttonClassName="py-2 text-13" options={EXPORTERS_LIST.map((service) => ({
> key: service.provider,
{EXPORTERS_LIST.map((service) => ( title: <span className="truncate">{t(service.i18n_title)}</span>,
<CustomSelect.Option key={service.provider} className="flex items-center gap-2" value={service}> isChecked: value.provider === service.provider,
<span className="truncate">{t(service.i18n_title)}</span> onClick: () => onChange(service),
</CustomSelect.Option> }))}
))} />
</CustomSelect>
)} )}
/> />
} }
/> />
<div className="px-4 py-3"> <div className="px-4 py-3">
<Button variant="primary" size="lg" type="submit" loading={exportLoading}> <Button
variant="ghost"
size="lg"
type="submit"
loading={exportLoading}
className="nodedc-settings-primary-button min-w-[9.75rem]"
>
{exportLoading ? `${t("workspace_settings.settings.exports.exporting")}...` : t("export")} {exportLoading ? `${t("workspace_settings.settings.exports.exporting")}...` : t("export")}
</Button> </Button>
</div> </div>

View File

@ -68,7 +68,7 @@ export const PrevExports = observer(function PrevExports(props: Props) {
<div className="flex items-center justify-between border-b border-subtle pb-3.5"> <div className="flex items-center justify-between border-b border-subtle pb-3.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="text-h6-medium text-primary">{t("workspace_settings.settings.exports.previous_exports")}</h3> <h3 className="text-h6-medium text-primary">{t("workspace_settings.settings.exports.previous_exports")}</h3>
<Button variant="tertiary" className="shrink-0" onClick={handleRefresh}> <Button variant="ghost" size="lg" className="nodedc-settings-chip shrink-0" onClick={handleRefresh}>
<RefreshCw className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} /> <RefreshCw className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />
{refreshing ? t("refreshing") : t("refresh_status")} {refreshing ? t("refreshing") : t("refresh_status")}
</Button> </Button>
@ -76,8 +76,9 @@ export const PrevExports = observer(function PrevExports(props: Props) {
{!!exporterServices?.results?.length && ( {!!exporterServices?.results?.length && (
<div className="flex items-center gap-2 text-11"> <div className="flex items-center gap-2 text-11">
<Button <Button
variant="secondary" variant="ghost"
size="sm" size="lg"
className="nodedc-settings-chip"
disabled={!exporterServices?.prev_page_results} disabled={!exporterServices?.prev_page_results}
onClick={() => exporterServices?.prev_page_results && setCursor(exporterServices?.prev_cursor)} onClick={() => exporterServices?.prev_page_results && setCursor(exporterServices?.prev_cursor)}
prependIcon={<MoveLeft />} prependIcon={<MoveLeft />}
@ -85,8 +86,9 @@ export const PrevExports = observer(function PrevExports(props: Props) {
{t("prev")} {t("prev")}
</Button> </Button>
<Button <Button
variant="secondary" variant="ghost"
size="sm" size="lg"
className="nodedc-settings-chip"
disabled={!exporterServices?.next_page_results} disabled={!exporterServices?.next_page_results}
onClick={() => exporterServices?.next_page_results && setCursor(exporterServices?.next_cursor)} onClick={() => exporterServices?.next_page_results && setCursor(exporterServices?.next_cursor)}
appendIcon={<MoveRight />} appendIcon={<MoveRight />}
@ -100,7 +102,7 @@ export const PrevExports = observer(function PrevExports(props: Props) {
{exporterServices && exporterServices?.results ? ( {exporterServices && exporterServices?.results ? (
exporterServices?.results?.length > 0 ? ( exporterServices?.results?.length > 0 ? (
<div> <div>
<div className="divide-y divide-subtle-1"> <div className="nodedc-settings-card overflow-hidden px-1 py-1">
<Table <Table
columns={columns} columns={columns}
data={exporterServices?.results ?? []} data={exporterServices?.results ?? []}

View File

@ -41,7 +41,7 @@ export const StartOfWeekPreference = observer(function StartOfWeekPreference(pro
<SelectionDropdown <SelectionDropdown
placement="bottom-end" placement="bottom-end"
menuButton={getStartOfWeekLabel(userProfile.start_of_the_week)} menuButton={getStartOfWeekLabel(userProfile.start_of_the_week)}
menuButtonWrapperClassName="flex w-full items-center justify-between rounded-full border border-subtle-1 px-3 py-2 text-13" menuButtonWrapperClassName="nodedc-settings-select flex w-full items-center justify-between px-4 py-3 text-13 font-medium"
options={START_OF_THE_WEEK_OPTIONS.map((day) => ({ options={START_OF_THE_WEEK_OPTIONS.map((day) => ({
key: `${day.value}`, key: `${day.value}`,
title: day.label, title: day.label,

View File

@ -81,7 +81,7 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro
projectId={projectId} projectId={projectId}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
/> />
<div className="flex items-center justify-between gap-4 overflow-x-hidden border-b border-subtle py-2"> <div className="flex items-center justify-between gap-4 overflow-x-hidden pb-4">
<div className="text-14 font-semibold">{t("common.members")}</div> <div className="text-14 font-semibold">{t("common.members")}</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="nodedc-settings-field flex min-h-[2.75rem] items-center justify-start gap-1.5 px-3"> <div className="nodedc-settings-field flex min-h-[2.75rem] items-center justify-start gap-1.5 px-3">
@ -116,7 +116,7 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro
{!projectMemberIds ? ( {!projectMemberIds ? (
<MembersSettingsLoader /> <MembersSettingsLoader />
) : ( ) : (
<div className="divide-y divide-subtle overflow-scroll"> <div className="nodedc-settings-card overflow-scroll px-1 py-1">
{searchedProjectMembers.length !== 0 && ( {searchedProjectMembers.length !== 0 && (
<ProjectMemberListItem <ProjectMemberListItem
memberDetails={memberDetails ?? []} memberDetails={memberDetails ?? []}

View File

@ -61,11 +61,11 @@ export const ActivityProfileSettingsList = observer(function ProfileActivityList
return ( return (
<> <>
{userProfileActivity ? ( {userProfileActivity ? (
<ul> <ul className="space-y-2">
{userProfileActivity.results.map((activityItem: any) => { {userProfileActivity.results.map((activityItem: any) => {
if (activityItem.field === "comment") if (activityItem.field === "comment")
return ( return (
<div key={activityItem.id} className="mt-2"> <div key={activityItem.id} className="nodedc-settings-card px-5 py-4">
<div className="relative flex items-start space-x-3"> <div className="relative flex items-start space-x-3">
<div className="relative px-1"> <div className="relative px-1">
{activityItem.field ? ( {activityItem.field ? (
@ -90,12 +90,12 @@ export const ActivityProfileSettingsList = observer(function ProfileActivityList
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div> <div>
<div className="text-11"> <div className="text-11 text-primary">
{activityItem.actor_detail.is_bot {activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name + " Bot" ? activityItem.actor_detail.first_name + " Bot"
: activityItem.actor_detail.display_name} : activityItem.actor_detail.display_name}
</div> </div>
<p className="mt-0.5 text-11 text-secondary"> <p className="mt-0.5 text-11 text-tertiary">
Commented {calculateTimeAgo(activityItem.created_at)} Commented {calculateTimeAgo(activityItem.created_at)}
</p> </p>
</div> </div>
@ -122,7 +122,7 @@ export const ActivityProfileSettingsList = observer(function ProfileActivityList
if ("field" in activityItem && activityItem.field !== "updated_by") if ("field" in activityItem && activityItem.field !== "updated_by")
return ( return (
<li key={activityItem.id}> <li key={activityItem.id}>
<div className="relative pb-1"> <div className="nodedc-settings-card relative px-5 py-4">
<div className="relative flex items-start space-x-2"> <div className="relative flex items-start space-x-2">
<> <>
<div> <div>
@ -153,7 +153,7 @@ export const ActivityProfileSettingsList = observer(function ProfileActivityList
</div> </div>
</div> </div>
</div> </div>
<div className="min-w-0 flex-1 border-b border-subtle py-4"> <div className="min-w-0 flex-1 py-2">
<div className="text-caption-md-regular break-words text-secondary"> <div className="text-caption-md-regular break-words text-secondary">
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? ( {activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
<span className="text-gray font-medium">NODE.DC</span> <span className="text-gray font-medium">NODE.DC</span>

View File

@ -82,10 +82,16 @@ export const ActivityProfileSettings = observer(function ActivityProfileSettings
title={t("account_settings.activity.heading")} title={t("account_settings.activity.heading")}
description={t("account_settings.activity.description")} description={t("account_settings.activity.description")}
/> />
<div className="mt-7 w-full">{activityPages}</div> <div className="mt-7 w-full space-y-2">{activityPages}</div>
{isLoadMoreVisible && ( {isLoadMoreVisible && (
<div className="mt-4 flex w-full items-center justify-center"> <div className="mt-4 flex w-full items-center justify-center">
<Button variant="ghost" onClick={handleLoadMore} appendIcon={<ChevronDown />}> <Button
variant="ghost"
size="lg"
className="nodedc-settings-primary-button min-w-[10.5rem]"
onClick={handleLoadMore}
appendIcon={<ChevronDown />}
>
{t("load_more")} {t("load_more")}
</Button> </Button>
</div> </div>

View File

@ -41,20 +41,23 @@ export const APITokensProfileSettings = observer(function APITokensProfileSettin
title={t("account_settings.api_tokens.heading")} title={t("account_settings.api_tokens.heading")}
description={t("account_settings.api_tokens.description")} description={t("account_settings.api_tokens.description")}
control={ control={
<Button variant="primary" size="lg" onClick={() => setIsCreateTokenModalOpen(true)}> <Button
variant="ghost"
size="lg"
className="nodedc-settings-primary-button min-w-[11rem]"
onClick={() => setIsCreateTokenModalOpen(true)}
>
{t("workspace_settings.settings.api_tokens.add_token")} {t("workspace_settings.settings.api_tokens.add_token")}
</Button> </Button>
} }
/> />
<div className="mt-7"> <div className="mt-7">
{tokens.length > 0 ? ( {tokens.length > 0 ? (
<> <div className="space-y-2">
<div>
{tokens.map((token) => ( {tokens.map((token) => (
<ApiTokenListItem key={token.id} token={token} /> <ApiTokenListItem key={token.id} token={token} />
))} ))}
</div> </div>
</>
) : ( ) : (
<EmptyStateCompact <EmptyStateCompact
assetKey="token" assetKey="token"

View File

@ -211,36 +211,15 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
/> />
<form onSubmit={handleSubmit(onSubmit)} className="w-full"> <form onSubmit={handleSubmit(onSubmit)} className="w-full">
<div className="flex w-full flex-col gap-7"> <div className="flex w-full flex-col gap-7">
<div className="relative h-44 w-full"> <div className="nodedc-settings-card overflow-hidden">
<div className="relative h-44 w-full overflow-hidden rounded-[1.35rem]">
<CoverImage <CoverImage
src={userCover} src={userCover}
className="h-44 w-full rounded-lg" className="h-44 w-full rounded-[1.35rem]"
alt={currentUser?.first_name ?? "Cover image"} alt={currentUser?.first_name ?? "Cover image"}
/> />
<div className="absolute -bottom-6 left-6 flex items-end justify-between"> <div className="absolute inset-0 bg-gradient-to-b from-black/10 via-transparent to-black/45" />
<div className="flex gap-3"> <div className="absolute right-4 bottom-4 flex">
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-surface-2">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
{!userAvatar || userAvatar === "" ? (
<div className="h-16 w-16 rounded-md bg-layer-1 p-2">
<CircleUserRound className="h-full w-full text-secondary" />
</div>
) : (
<div className="relative h-16 w-16 overflow-hidden">
<img
src={getFileURL(userAvatar)}
className="absolute top-0 left-0 h-full w-full rounded-lg object-cover"
onClick={() => setIsImageUploadModalOpen(true)}
alt={currentUser?.display_name}
role="button"
/>
</div>
)}
</button>
</div>
</div>
</div>
<div className="absolute right-3 bottom-3 flex">
<Controller <Controller
control={control} control={control}
name="cover_image_url" name="cover_image_url"
@ -251,23 +230,42 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
onChange={(imageUrl) => onChange(imageUrl)} onChange={(imageUrl) => onChange(imageUrl)}
value={value} value={value}
isProfileCover isProfileCover
buttonClassName="nodedc-overlay-button min-w-[10.5rem]"
/> />
)} )}
/> />
</div> </div>
</div> </div>
<div className="item-center mt-6 flex justify-between"> <div className="-mt-7 flex items-end justify-between gap-4 px-6 pb-6">
<div className="flex flex-col"> <div className="flex items-end gap-4">
<div className="item-center flex text-16 font-medium text-secondary"> <button type="button" onClick={() => setIsImageUploadModalOpen(true)} className="shrink-0">
<span>{`${watch("first_name")} ${watch("last_name")}`}</span> {!userAvatar || userAvatar === "" ? (
<div className="grid h-[5.5rem] w-[5.5rem] place-items-center rounded-[1.35rem] bg-white/8 p-3 backdrop-blur-xl">
<CircleUserRound className="h-full w-full text-primary" />
</div> </div>
<span className="text-13 tracking-tight text-tertiary">{watch("email")}</span> ) : (
<div className="relative h-[5.5rem] w-[5.5rem] overflow-hidden rounded-[1.35rem] bg-white/8">
<img
src={getFileURL(userAvatar)}
className="absolute top-0 left-0 h-full w-full rounded-[1.35rem] object-cover"
onClick={() => setIsImageUploadModalOpen(true)}
alt={currentUser?.display_name}
role="button"
/>
</div>
)}
</button>
<div className="flex flex-col gap-1 pb-1">
<div className="text-h4-semibold leading-6 text-primary">{`${watch("first_name")} ${watch("last_name")}`}</div>
<span className="text-body-sm-regular text-tertiary">{watch("email")}</span>
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> </div>
<div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2 xl:grid-cols-3"> </div>
<div className="flex flex-col gap-1"> <div className="nodedc-settings-card flex flex-col gap-6 px-5 py-5">
<h4 className="text-13 font-medium text-secondary"> <div className="grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2 xl:grid-cols-3">
<div className="flex flex-col gap-1.5">
<h4 className="text-13 font-medium text-tertiary">
{t("first_name")}&nbsp; {t("first_name")}&nbsp;
<span className="text-danger-primary">*</span> <span className="text-danger-primary">*</span>
</h4> </h4>
@ -288,7 +286,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
ref={ref} ref={ref}
hasError={Boolean(errors.first_name)} hasError={Boolean(errors.first_name)}
placeholder={t("profile_general.first_name_placeholder")} placeholder={t("profile_general.first_name_placeholder")}
className={`w-full rounded-md ${errors.first_name ? "border-danger-strong" : ""}`} className={`nodedc-settings-input w-full ${errors.first_name ? "border-danger-strong" : ""}`}
maxLength={50} maxLength={50}
autoComplete="on" autoComplete="on"
/> />
@ -296,8 +294,8 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
/> />
{errors.first_name && <span className="text-11 text-danger-primary">{errors.first_name.message}</span>} {errors.first_name && <span className="text-11 text-danger-primary">{errors.first_name.message}</span>}
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1.5">
<h4 className="text-13 font-medium text-secondary">{t("last_name")}</h4> <h4 className="text-13 font-medium text-tertiary">{t("last_name")}</h4>
<Controller <Controller
control={control} control={control}
name="last_name" name="last_name"
@ -314,7 +312,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
ref={ref} ref={ref}
hasError={Boolean(errors.last_name)} hasError={Boolean(errors.last_name)}
placeholder={t("profile_general.last_name_placeholder")} placeholder={t("profile_general.last_name_placeholder")}
className="w-full rounded-md" className="nodedc-settings-input w-full"
maxLength={50} maxLength={50}
autoComplete="on" autoComplete="on"
/> />
@ -322,8 +320,8 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
/> />
{errors.last_name && <span className="text-11 text-danger-primary">{errors.last_name.message}</span>} {errors.last_name && <span className="text-11 text-danger-primary">{errors.last_name.message}</span>}
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1.5">
<h4 className="text-13 font-medium text-secondary"> <h4 className="text-13 font-medium text-tertiary">
{t("display_name")}&nbsp; {t("display_name")}&nbsp;
<span className="text-danger-primary">*</span> <span className="text-danger-primary">*</span>
</h4> </h4>
@ -344,7 +342,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
ref={ref} ref={ref}
hasError={Boolean(errors?.display_name)} hasError={Boolean(errors?.display_name)}
placeholder={t("profile_general.display_name_placeholder")} placeholder={t("profile_general.display_name_placeholder")}
className={`w-full ${errors?.display_name ? "border-danger-strong" : ""}`} className={`nodedc-settings-input w-full ${errors?.display_name ? "border-danger-strong" : ""}`}
maxLength={50} maxLength={50}
/> />
)} )}
@ -353,8 +351,8 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
<span className="text-11 text-danger-primary">{errors?.display_name?.message}</span> <span className="text-11 text-danger-primary">{errors?.display_name?.message}</span>
)} )}
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1.5 xl:col-span-2">
<h4 className="text-13 font-medium text-secondary"> <h4 className="text-13 font-medium text-tertiary">
{t("auth.common.email.label")}&nbsp; {t("auth.common.email.label")}&nbsp;
<span className="text-danger-primary">*</span> <span className="text-danger-primary">*</span>
</h4> </h4>
@ -373,7 +371,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
ref={ref} ref={ref}
hasError={Boolean(errors.email)} hasError={Boolean(errors.email)}
placeholder={t("profile_general.email_placeholder")} placeholder={t("profile_general.email_placeholder")}
className={`w-full cursor-not-allowed rounded-md !bg-surface-2 ${ className={`nodedc-settings-input w-full cursor-not-allowed !bg-white/4 ${
errors.email ? "border-danger-strong" : "" errors.email ? "border-danger-strong" : ""
}`} }`}
autoComplete="on" autoComplete="on"
@ -384,7 +382,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
{isSMTPConfigured && ( {isSMTPConfigured && (
<button <button
type="button" type="button"
className="btn w-fit text-11 text-secondary underline" className="nodedc-settings-chip flex w-fit items-center gap-2 px-3.5 py-1.5 text-12 font-medium text-primary"
onClick={() => setIsChangeEmailModalOpen(true)} onClick={() => setIsChangeEmailModalOpen(true)}
> >
{t("account_settings.profile.change_email_modal.title")} {t("account_settings.profile.change_email_modal.title")}
@ -393,8 +391,15 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
</div> </div>
</div> </div>
</div> </div>
<div> <div className="flex items-center justify-between py-1">
<Button variant="primary" type="submit" loading={isLoading}> <div />
<Button
variant="ghost"
size="lg"
type="submit"
loading={isLoading}
className="nodedc-settings-save-button min-w-[12rem]"
>
{isLoading ? t("saving") : t("save_changes")} {isLoading ? t("saving") : t("save_changes")}
</Button> </Button>
</div> </div>
@ -405,7 +410,11 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
title={t("deactivate_account")} title={t("deactivate_account")}
description={t("deactivate_account_description")} description={t("deactivate_account_description")}
control={ control={
<Button variant="error-outline" onClick={() => setDeactivateAccountModal(true)}> <Button
variant="ghost"
className="nodedc-modal-danger-button min-w-[11rem]"
onClick={() => setDeactivateAccountModal(true)}
>
{t("deactivate_account")} {t("deactivate_account")}
</Button> </Button>
} }

View File

@ -59,7 +59,7 @@ export const NotificationsProfileSettingsForm = observer(function NotificationsP
}, [reset, data]); }, [reset, data]);
return ( return (
<div className="flex flex-col gap-y-1"> <div className="flex flex-col gap-y-2">
<SettingsControlItem <SettingsControlItem
title={t("property_changes")} title={t("property_changes")}
description={t("property_changes_description")} description={t("property_changes_description")}
@ -100,7 +100,7 @@ export const NotificationsProfileSettingsForm = observer(function NotificationsP
/> />
} }
/> />
<div className="border-l-3 border-subtle-1 pl-3"> <div className="ml-4 border-l border-white/8 pl-4">
<SettingsControlItem <SettingsControlItem
title={t("issue_completed")} title={t("issue_completed")}
description={t("issue_completed_description")} description={t("issue_completed_description")}

View File

@ -35,7 +35,7 @@ export const NotificationsProfileSettings = observer(function NotificationsProfi
title={t("account_settings.notifications.heading")} title={t("account_settings.notifications.heading")}
description={t("account_settings.notifications.description")} description={t("account_settings.notifications.description")}
/> />
<div className="mt-7"> <div className="mt-7 flex flex-col gap-3">
<NotificationsProfileSettingsForm data={data} /> <NotificationsProfileSettingsForm data={data} />
</div> </div>
</div> </div>

View File

@ -73,7 +73,14 @@ export const ProfileSettingsLanguageAndTimezonePreferencesList = observer(
<SettingsControlItem <SettingsControlItem
title={t("timezone")} title={t("timezone")}
description={t("timezone_setting")} description={t("timezone_setting")}
control={<TimezoneSelect value={user?.user_timezone || "Asia/Kolkata"} onChange={handleTimezoneChange} />} control={
<TimezoneSelect
value={user?.user_timezone || "Asia/Kolkata"}
onChange={handleTimezoneChange}
buttonClassName="nodedc-settings-select !border-0 !px-4 !py-3 text-13 font-medium"
className="!rounded-[1.25rem]"
/>
}
/> />
<SettingsControlItem <SettingsControlItem
title={t("language")} title={t("language")}
@ -87,7 +94,7 @@ export const ProfileSettingsLanguageAndTimezonePreferencesList = observer(
onClick: () => handleLanguageChange(item.value), onClick: () => handleLanguageChange(item.value),
}))} }))}
menuButton={profile?.language ? getLanguageLabel(profile?.language) : "Select a language"} menuButton={profile?.language ? getLanguageLabel(profile?.language) : "Select a language"}
menuButtonWrapperClassName="rounded-md border border-subtle-1 px-3 py-2 text-13" menuButtonWrapperClassName="nodedc-settings-select px-4 py-3 text-13 font-medium"
placement="bottom-end" placement="bottom-end"
/> />
} }

View File

@ -29,7 +29,7 @@ export const PreferencesProfileSettings = observer(function PreferencesProfileSe
description={t("account_settings.preferences.description")} description={t("account_settings.preferences.description")}
/> />
<div className="mt-7 flex w-full flex-col gap-6"> <div className="mt-7 flex w-full flex-col gap-6">
<section> <section className="flex flex-col gap-3">
<ProfileSettingsDefaultPreferencesList /> <ProfileSettingsDefaultPreferencesList />
</section> </section>
<section className="flex flex-col gap-y-3"> <section className="flex flex-col gap-y-3">

View File

@ -132,10 +132,10 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
<div className="size-full"> <div className="size-full">
<ProfileSettingsHeading title={t("auth.common.password.change_password.label.default")} /> <ProfileSettingsHeading title={t("auth.common.password.change_password.label.default")} />
<form onSubmit={handleSubmit(handleChangePassword)} className="mt-7 flex flex-col gap-8"> <form onSubmit={handleSubmit(handleChangePassword)} className="mt-7 flex flex-col gap-8">
<div className="flex flex-col gap-y-7"> <div className="nodedc-settings-card flex flex-col gap-y-7 px-5 py-5">
{oldPasswordRequired && ( {oldPasswordRequired && (
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-2">
<h4 className="text-13">{t("auth.common.password.current_password.label")}</h4> <h4 className="text-13 font-medium text-tertiary">{t("auth.common.password.current_password.label")}</h4>
<div className="relative flex items-center rounded-md"> <div className="relative flex items-center rounded-md">
<Controller <Controller
control={control} control={control}
@ -150,7 +150,7 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
value={value} value={value}
onChange={onChange} onChange={onChange}
placeholder={t("old_password")} placeholder={t("old_password")}
className="w-full" className="nodedc-settings-input w-full"
hasError={Boolean(errors.old_password)} hasError={Boolean(errors.old_password)}
autoComplete="current-password" autoComplete="current-password"
/> />
@ -175,7 +175,7 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
)} )}
<div className="grid gap-x-4 gap-y-7 sm:grid-cols-2"> <div className="grid gap-x-4 gap-y-7 sm:grid-cols-2">
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-2">
<h4 className="text-13">{t("auth.common.password.new_password.label")}</h4> <h4 className="text-13 font-medium text-tertiary">{t("auth.common.password.new_password.label")}</h4>
<div className="relative flex items-center rounded-md"> <div className="relative flex items-center rounded-md">
<Controller <Controller
control={control} control={control}
@ -190,7 +190,7 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
value={value} value={value}
placeholder={t("auth.common.password.new_password.placeholder")} placeholder={t("auth.common.password.new_password.placeholder")}
onChange={onChange} onChange={onChange}
className="w-full" className="nodedc-settings-input w-full"
hasError={Boolean(errors.new_password)} hasError={Boolean(errors.new_password)}
onFocus={() => setIsPasswordInputFocused(true)} onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)} onBlur={() => setIsPasswordInputFocused(false)}
@ -221,7 +221,7 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
)} )}
</div> </div>
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-2">
<h4 className="text-13">{t("auth.common.password.confirm_password.label")}</h4> <h4 className="text-13 font-medium text-tertiary">{t("auth.common.password.confirm_password.label")}</h4>
<div className="relative flex items-center rounded-md"> <div className="relative flex items-center rounded-md">
<Controller <Controller
control={control} control={control}
@ -236,7 +236,7 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
placeholder={t("auth.common.password.confirm_password.placeholder")} placeholder={t("auth.common.password.confirm_password.placeholder")}
value={value} value={value}
onChange={onChange} onChange={onChange}
className="w-full" className="nodedc-settings-input w-full"
hasError={Boolean(errors.confirm_password)} hasError={Boolean(errors.confirm_password)}
onFocus={() => setIsRetryPasswordInputFocused(true)} onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)} onBlur={() => setIsRetryPasswordInputFocused(false)}
@ -262,7 +262,14 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
</div> </div>
</div> </div>
<div> <div>
<Button variant="primary" size="xl" type="submit" loading={isSubmitting} disabled={isButtonDisabled}> <Button
variant="ghost"
size="lg"
type="submit"
loading={isSubmitting}
disabled={isButtonDisabled}
className="nodedc-settings-save-button min-w-[12rem]"
>
{isSubmitting {isSubmitting
? `${t("auth.common.password.change_password.label.submitting")}` ? `${t("auth.common.password.change_password.label.submitting")}`
: t("auth.common.password.change_password.label.default")} : t("auth.common.password.change_password.label.default")}

View File

@ -39,19 +39,25 @@ export const ProfileSettingsModal = observer(function ProfileSettingsModal() {
handleClose={handleClose} handleClose={handleClose}
position={EModalPosition.CENTER} position={EModalPosition.CENTER}
width={EModalWidth.VIXL} width={EModalWidth.VIXL}
className="h-175" className="nodedc-glass-modal h-175 overflow-hidden rounded-[1.85rem] border-0 shadow-none"
> >
<div className="@container relative size-full"> <div className="@container relative size-full overflow-hidden rounded-[1.85rem]">
<div className="flex size-full"> <div className="flex size-full">
<ProfileSettingsSidebarRoot <ProfileSettingsSidebarRoot
activeTab={activeTab} activeTab={activeTab}
className="w-[250px] rounded-l-xl" className="w-[280px] rounded-l-[1.85rem]"
updateActiveTab={(tab) => toggleProfileSettingsModal({ activeTab: tab })} updateActiveTab={(tab) => toggleProfileSettingsModal({ activeTab: tab })}
/> />
<ProfileSettingsContent activeTab={activeTab} className="flex-1 rounded-r-xl" /> <ProfileSettingsContent activeTab={activeTab} className="flex-1 rounded-r-[1.85rem]" />
</div> </div>
<div className="absolute top-3.5 right-3.5"> <div className="absolute top-3.5 right-3.5">
<IconButton size="base" variant="tertiary" icon={X} onClick={handleClose} /> <IconButton
size="base"
variant="ghost"
icon={X}
onClick={handleClose}
className="nodedc-overlay-button !h-11 !w-11 !min-h-11 !rounded-[1.1rem] !px-0"
/>
</div> </div>
</div> </div>
</ModalCore> </ModalCore>

View File

@ -105,7 +105,12 @@ export function CreateWebhookModal(props: ICreateWebhookModal) {
}); });
return ( return (
<ModalCore isOpen={isOpen} position={EModalPosition.TOP} width={EModalWidth.XXL} className="p-4 pb-0"> <ModalCore
isOpen={isOpen}
position={EModalPosition.TOP}
width={EModalWidth.XXL}
className="nodedc-glass-modal rounded-[1.75rem] p-4 pb-0"
>
{!generatedWebhook ? ( {!generatedWebhook ? (
<WebhookForm onSubmit={handleCreateWebhook} handleClose={handleClose} /> <WebhookForm onSubmit={handleCreateWebhook} handleClose={handleClose} />
) : ( ) : (

View File

@ -17,17 +17,15 @@ type Props = {
export function WebhooksEmptyState(props: Props) { export function WebhooksEmptyState(props: Props) {
const { onClick } = props; const { onClick } = props;
return ( return (
<div <div className="mx-auto flex w-full max-w-[34rem] flex-col items-center text-center">
className={`mx-auto flex w-full items-center justify-center rounded-xs border border-subtle bg-surface-2 px-16 py-10 lg:w-3/4`} <img src={EmptyWebhook} className="w-40 object-cover opacity-90 sm:w-48" alt="empty" />
> <h6 className="mt-6 text-2xl font-semibold text-primary">No webhooks</h6>
<div className="flex w-full flex-col items-center text-center"> <p className="mt-3 max-w-[28rem] text-body-sm-regular text-tertiary">
<img src={EmptyWebhook} className="w-52 object-cover sm:w-60" alt="empty" /> Create webhooks to receive real-time updates and automate actions.
<h6 className="mt-6 mb-3 text-18 font-semibold sm:mt-8">No webhooks</h6> </p>
<p className="mb-7 text-tertiary sm:mb-8">Create webhooks to receive real-time updates and automate actions</p> <Button variant="ghost" size="lg" className="nodedc-settings-primary-button mt-7 min-w-[11rem]" onClick={onClick}>
<Button className="flex items-center gap-1.5" onClick={onClick}>
Add webhook Add webhook
</Button> </Button>
</div> </div>
</div>
); );
} }

View File

@ -69,7 +69,7 @@ export const WebhookForm = observer(function WebhookForm(props: Props) {
return ( return (
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-5"> <div className="space-y-5">
<div className="text-18 font-medium text-secondary"> <div className="text-18 font-medium text-primary">
{data {data
? t("workspace_settings.settings.webhooks.modal.details") ? t("workspace_settings.settings.webhooks.modal.details")
: t("workspace_settings.settings.webhooks.modal.title")} : t("workspace_settings.settings.webhooks.modal.title")}
@ -99,9 +99,11 @@ export const WebhookForm = observer(function WebhookForm(props: Props) {
<div className="space-y-5 pt-0"> <div className="space-y-5 pt-0">
<WebhookSecretKey data={data} /> <WebhookSecretKey data={data} />
<Button <Button
variant="ghost"
size="lg" size="lg"
type="submit" type="submit"
loading={isSubmitting} loading={isSubmitting}
className="nodedc-settings-save-button min-w-[11rem]"
data-ph-element={WORKSPACE_SETTINGS_TRACKER_ELEMENTS.WEBHOOK_UPDATE_BUTTON} data-ph-element={WORKSPACE_SETTINGS_TRACKER_ELEMENTS.WEBHOOK_UPDATE_BUTTON}
> >
{isSubmitting ? t("updating") : t("update")} {isSubmitting ? t("updating") : t("update")}
@ -109,11 +111,17 @@ export const WebhookForm = observer(function WebhookForm(props: Props) {
</div> </div>
) : ( ) : (
<div className="flex items-center justify-end gap-2 border-t-[0.5px] border-subtle px-5 py-4"> <div className="flex items-center justify-end gap-2 border-t-[0.5px] border-subtle px-5 py-4">
<Button variant="secondary" size="lg" onClick={handleClose}> <Button variant="secondary" size="lg" className="nodedc-modal-secondary-button" onClick={handleClose}>
{t("cancel")} {t("cancel")}
</Button> </Button>
{!webhookSecretKey && ( {!webhookSecretKey && (
<Button type="submit" variant="primary" size="lg" loading={isSubmitting} className="capitalize"> <Button
type="submit"
variant="primary"
size="lg"
loading={isSubmitting}
className="nodedc-modal-primary-button min-w-[9.5rem] capitalize"
>
{isSubmitting ? t("common.creating") : t("common.create")} {isSubmitting ? t("common.creating") : t("common.create")}
</Button> </Button>
)} )}

View File

@ -29,12 +29,12 @@ export function WebhooksListItem(props: IWebhookListItem) {
}; };
return ( return (
<div className="rounded-lg border border-subtle bg-layer-2 px-4 py-3"> <div className="nodedc-settings-card px-4 py-3">
<Link <Link
href={`/${workspaceSlug}/settings/webhooks/${webhook?.id}`} href={`/${workspaceSlug}/settings/webhooks/${webhook?.id}`}
className="flex items-center justify-between gap-4" className="flex items-center justify-between gap-4"
> >
<h5 className="truncate text-body-sm-medium">{webhook.url}</h5> <h5 className="truncate text-body-sm-medium text-primary">{webhook.url}</h5>
<div className="shrink-0"> <div className="shrink-0">
<ToggleSwitch value={webhook.is_active} onChange={handleToggle} /> <ToggleSwitch value={webhook.is_active} onChange={handleToggle} />
</div> </div>

View File

@ -15,7 +15,7 @@ export const WebhooksList = observer(function WebhooksList() {
const { webhooks } = useWebhook(); const { webhooks } = useWebhook();
return ( return (
<div className="flex size-full flex-col gap-y-2 overflow-y-auto rounded-lg border border-subtle bg-layer-1 p-3"> <div className="nodedc-settings-card flex size-full flex-col gap-y-3 overflow-y-auto p-3">
{Object.values(webhooks ?? {}).map((webhook) => ( {Object.values(webhooks ?? {}).map((webhook) => (
<WebhooksListItem key={webhook.id} webhook={webhook} /> <WebhooksListItem key={webhook.id} webhook={webhook} />
))} ))}

View File

@ -122,17 +122,17 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
}} }}
onSubmit={handleRemoveInvitation} onSubmit={handleRemoveInvitation}
/> />
<div className="group flex h-full w-full items-center justify-between px-3 py-4 hover:bg-layer-transparent-hover"> <div className="group flex h-full w-full items-center justify-between rounded-[1rem] px-4 py-4 transition-colors hover:bg-white/4">
<div className="flex items-center gap-x-4 gap-y-2"> <div className="flex items-center gap-x-4 gap-y-2">
<span className="relative flex h-10 w-10 items-center justify-center rounded-sm bg-layer-3 p-4 text-tertiary capitalize"> <span className="relative flex h-10 w-10 items-center justify-center rounded-[0.95rem] bg-white/6 p-4 text-tertiary capitalize">
{(invitationDetails.email ?? "?")[0]} {(invitationDetails.email ?? "?")[0]}
</span> </span>
<div> <div>
<h4 className="cursor-default text-body-xs-regular">{invitationDetails.email}</h4> <h4 className="cursor-default text-body-xs-regular text-primary">{invitationDetails.email}</h4>
</div> </div>
</div> </div>
<div className="flex items-center gap-2 text-11"> <div className="flex items-center gap-2 text-11">
<div className="flex items-center justify-center rounded-sm bg-label-yellow-bg-strong/20 px-2.5 py-1 text-center text-caption-sm-medium text-label-yellow-text"> <div className="flex items-center justify-center rounded-full bg-label-yellow-bg-strong/20 px-2.5 py-1 text-center text-caption-sm-medium text-label-yellow-text">
<p>{t("common.pending")}</p> <p>{t("common.pending")}</p>
</div> </div>
<SelectionDropdown <SelectionDropdown
@ -167,7 +167,7 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
}, },
}))} }))}
menuButton={ menuButton={
<div className="item-center flex gap-1 rounded-sm px-2 py-0.5"> <div className="nodedc-settings-chip item-center flex gap-1 px-3 py-1">
<span <span
className={`flex items-center rounded-sm text-caption-sm-medium ${ className={`flex items-center rounded-sm text-caption-sm-medium ${
hasRoleChangeAccess ? "" : "text-placeholder" hasRoleChangeAccess ? "" : "text-placeholder"

View File

@ -91,7 +91,7 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
if (isEmpty(columns)) return <MembersLayoutLoader />; if (isEmpty(columns)) return <MembersLayoutLoader />;
return ( return (
<div className="grid border-t border-subtle"> <div className="grid overflow-hidden rounded-[1.2rem]">
{removeMemberModal && ( {removeMemberModal && (
<ConfirmWorkspaceMemberRemove <ConfirmWorkspaceMemberRemove
isOpen={removeMemberModal.member.id.length > 0} isOpen={removeMemberModal.member.id.length > 0}
@ -109,10 +109,10 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
(memberDetails?.filter((member): member is IWorkspaceMember => member !== null) ?? []) as unknown as RowData[] (memberDetails?.filter((member): member is IWorkspaceMember => member !== null) ?? []) as unknown as RowData[]
} }
keyExtractor={(rowData) => rowData?.member.id ?? ""} keyExtractor={(rowData) => rowData?.member.id ?? ""}
tHeadClassName="border-b border-subtle" tHeadClassName="border-b border-white/6"
thClassName="text-left font-medium divide-x-0 text-placeholder" thClassName="text-left font-medium divide-x-0 text-placeholder"
tBodyClassName="divide-y-0" tBodyClassName="divide-y-0"
tBodyTrClassName="divide-x-0 p-4 h-10 text-secondary" tBodyTrClassName="divide-x-0 h-11 px-4 text-secondary"
tHeadTrClassName="divide-x-0" tHeadTrClassName="divide-x-0"
/> />
</div> </div>

View File

@ -72,7 +72,7 @@ export const WorkspaceMembersList = observer(function WorkspaceMembersList(props
return ( return (
<> <>
<div className="divide-y-[0.5px] divide-subtle overflow-scroll"> <div className="nodedc-settings-card overflow-hidden px-1 py-1">
{searchedMemberIds?.length !== 0 && <WorkspaceMembersListItem memberDetails={memberDetails ?? []} />} {searchedMemberIds?.length !== 0 && <WorkspaceMembersListItem memberDetails={memberDetails ?? []} />}
{searchedInvitationsIds?.length === 0 && searchedMemberIds?.length === 0 && ( {searchedInvitationsIds?.length === 0 && searchedMemberIds?.length === 0 && (
<h4 className="mt-16 text-center text-body-xs-regular text-placeholder">{t("no_matching_members")}</h4> <h4 className="mt-16 text-center text-body-xs-regular text-placeholder">{t("no_matching_members")}</h4>
@ -85,7 +85,7 @@ export const WorkspaceMembersList = observer(function WorkspaceMembersList(props
buttonClassName="w-full" buttonClassName="w-full"
className="" className=""
title={ title={
<div className="flex w-full items-center justify-between pt-4"> <div className="flex w-full items-center justify-between pt-5">
<div className="flex"> <div className="flex">
<h4 className="pt-2 pb-2 text-h5-medium">{t("workspace_settings.settings.members.pending_invites")}</h4> <h4 className="pt-2 pb-2 text-h5-medium">{t("workspace_settings.settings.members.pending_invites")}</h4>
{searchedInvitationsIds && ( {searchedInvitationsIds && (
@ -97,7 +97,7 @@ export const WorkspaceMembersList = observer(function WorkspaceMembersList(props
} }
> >
<Disclosure.Panel> <Disclosure.Panel>
<div className="ml-auto items-center gap-1.5 rounded-md bg-surface-1 py-1.5"> <div className="nodedc-settings-card ml-auto items-center gap-1.5 py-2">
{searchedInvitationsIds?.map((invitationId) => ( {searchedInvitationsIds?.map((invitationId) => (
<WorkspaceInvitationsListItem key={invitationId} invitationId={invitationId} /> <WorkspaceInvitationsListItem key={invitationId} invitationId={invitationId} />
))} ))}

View File

@ -147,27 +147,27 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
)} )}
/> />
<div className={cn("flex w-full flex-col gap-y-7", { "opacity-60": !isAdmin })}> <div className={cn("flex w-full flex-col gap-y-7", { "opacity-60": !isAdmin })}>
<div className="flex items-center gap-5"> <div className="nodedc-settings-card flex items-center gap-5 px-5 py-5">
<div className="flex shrink-0 flex-col gap-1"> <div className="flex shrink-0 flex-col gap-1">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)} disabled={!isAdmin}> <button type="button" onClick={() => setIsImageUploadModalOpen(true)} disabled={!isAdmin}>
{workspaceLogo && workspaceLogo !== "" ? ( {workspaceLogo && workspaceLogo !== "" ? (
<div className="relative flex size-14"> <div className="relative flex size-14">
<img <img
src={getFileURL(workspaceLogo)} src={getFileURL(workspaceLogo)}
className="absolute top-0 left-0 size-full rounded-md object-cover" className="absolute top-0 left-0 size-full rounded-[1rem] object-cover"
alt="Workspace Logo" alt="Workspace Logo"
/> />
</div> </div>
) : ( ) : (
<div className="relative grid size-14 place-items-center rounded-md bg-accent-primary text-24 text-on-color uppercase"> <div className="relative grid size-14 place-items-center rounded-[1rem] bg-accent-primary text-24 text-on-color uppercase">
{currentWorkspace?.name?.charAt(0) ?? "N"} {currentWorkspace?.name?.charAt(0) ?? "N"}
</div> </div>
)} )}
</button> </button>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1.5">
<div className="mb:-my-5 text-h5-semibold leading-6">{watch("name")}</div> <div className="text-h5-semibold leading-6 text-primary">{watch("name")}</div>
<button type="button" onClick={handleCopyUrl} className="text-left text-body-xs-regular tracking-tight">{`${ <button type="button" onClick={handleCopyUrl} className="text-left text-body-xs-regular tracking-tight text-tertiary">{`${
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "") typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
}/${currentWorkspace.slug}`}</button> }/${currentWorkspace.slug}`}</button>
{isAdmin && ( {isAdmin && (
@ -188,9 +188,9 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
)} )}
</div> </div>
</div> </div>
<div className="flex flex-col gap-7"> <div className="nodedc-settings-card flex flex-col gap-7 px-5 py-5">
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-10 xl:grid-cols-2 2xl:grid-cols-3"> <div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-8 xl:grid-cols-2 2xl:grid-cols-3">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2.5">
<h4 className="text-body-sm-medium text-tertiary">{t("workspace_settings.settings.general.name")}</h4> <h4 className="text-body-sm-medium text-tertiary">{t("workspace_settings.settings.general.name")}</h4>
<Controller <Controller
control={control} control={control}
@ -208,14 +208,14 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
ref={ref} ref={ref}
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
placeholder={t("workspace_settings.settings.general.name")} placeholder={t("workspace_settings.settings.general.name")}
className="w-full rounded-md" className="nodedc-settings-input w-full"
disabled={!isAdmin} disabled={!isAdmin}
/> />
)} )}
/> />
{errors.name && <p className="text-caption-sm-regular text-danger-primary">{errors.name.message}</p>} {errors.name && <p className="text-caption-sm-regular text-danger-primary">{errors.name.message}</p>}
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2.5">
<h4 className="text-body-sm-medium text-tertiary"> <h4 className="text-body-sm-medium text-tertiary">
{t("workspace_settings.settings.general.company_size")} {t("workspace_settings.settings.general.company_size")}
</h4> </h4>
@ -234,13 +234,13 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
ORGANIZATION_SIZE.find((c) => c === value) ?? ORGANIZATION_SIZE.find((c) => c === value) ??
t("workspace_settings.settings.general.errors.company_size.select_a_range") t("workspace_settings.settings.general.errors.company_size.select_a_range")
} }
menuButtonWrapperClassName="rounded-md border border-subtle bg-layer-2 px-3 py-2 text-13 shadow-none" menuButtonWrapperClassName="nodedc-settings-select px-4 py-3 text-13 font-medium"
disabled={!isAdmin} disabled={!isAdmin}
/> />
)} )}
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2.5">
<h4 className="text-body-sm-medium text-tertiary">{t("workspace_settings.settings.general.url")}</h4> <h4 className="text-body-sm-medium text-tertiary">{t("workspace_settings.settings.general.url")}</h4>
<Controller <Controller
control={control} control={control}
@ -257,13 +257,13 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
onChange={onChange} onChange={onChange}
ref={ref} ref={ref}
hasError={Boolean(errors.url)} hasError={Boolean(errors.url)}
className="w-full cursor-not-allowed rounded-md !bg-layer-1" className="nodedc-settings-input w-full cursor-not-allowed !bg-white/4"
disabled disabled
/> />
)} )}
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2.5">
<h4 className="text-body-sm-medium text-tertiary"> <h4 className="text-body-sm-medium text-tertiary">
{t("workspace_settings.settings.general.workspace_timezone")} {t("workspace_settings.settings.general.workspace_timezone")}
</h4> </h4>
@ -280,10 +280,11 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
</div> </div>
</div> </div>
{isAdmin && ( {isAdmin && (
<div className="flex items-center justify-between py-2"> <div className="flex items-center justify-between py-1">
<Button <Button
variant="primary" variant="primary"
size="lg" size="lg"
className="nodedc-settings-save-button min-w-[13rem]"
onClick={(e) => { onClick={(e) => {
void handleSubmit(onSubmit)(e); void handleSubmit(onSubmit)(e);
}} }}