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,
})}
>
<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">
{t("workspace_settings.settings.members.title")}
{workspaceMemberIds && workspaceMemberIds.length > 0 && (
@ -121,7 +121,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
)}
</h4>
<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" />
<input
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} />
{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")}
</Button>
)}

View File

@ -12,13 +12,13 @@ import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
// components
import { EmptyStateCompact } from "@plane/propel/empty-state";
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
import { SettingsHeading } from "@/components/settings/heading";
import { WebhookSettingsLoader } from "@/components/ui/loader/settings/web-hook";
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks";
import { WebhooksEmptyState } from "@/components/web-hooks/empty-state";
// hooks
import { useWebhook } from "@/hooks/store/use-webhook";
import { useWorkspace } from "@/hooks/store/use-workspace";
@ -78,7 +78,12 @@ function WebhooksListPage({ params }: Route.ComponentProps) {
title={t("workspace_settings.settings.webhooks.title")}
description={t("workspace_settings.settings.webhooks.description")}
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")}
</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 items-center justify-center">
<EmptyStateCompact
assetKey="webhook"
title={t("settings_empty_state.webhooks.title")}
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 className="py-20">
<WebhooksEmptyState onClick={() => setShowCreateWebhookModal(true)} />
</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")}
control={
<Button
variant="error-outline"
variant="ghost"
className="nodedc-modal-danger-button min-w-[10rem]"
onClick={() => setDeleteWorkspaceModal(true)}
data-ph-element={WORKSPACE_TRACKER_ELEMENTS.DELETE_WORKSPACE_BUTTON}
>

View File

@ -30,31 +30,31 @@ export function ApiTokenListItem(props: Props) {
return (
<>
<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}>
<button
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}
>
<XCircle className="h-4 w-4 text-danger-primary" />
</button>
</Tooltip>
<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
className={`${
token.is_active ? "bg-success-subtle text-success-primary" : "bg-layer-1 text-placeholder"
} ml-2 flex h-4 max-h-fit items-center rounded-xs px-2 text-11 font-medium`}
token.is_active ? "bg-success-subtle text-success-primary" : "bg-white/6 text-placeholder"
} 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"}
</span>
</div>
<div className="mt-1 flex w-full flex-col justify-center">
{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.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 { EIssuesStoreType } 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 { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
import { SelectionDropdown } from "@/components/common/selection-dropdown";
import { useProject } from "@/hooks/store/use-project";
import { useUser, useUserPermissions } from "@/hooks/store/user";
import { ProjectExportService } from "@/services/project/project-export.service";
@ -171,6 +172,8 @@ export const ExportForm = observer(function ExportForm(props: Props) {
.join(", ")
: "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]"
placement="bottom-end"
multiple
@ -189,26 +192,30 @@ export const ExportForm = observer(function ExportForm(props: Props) {
name="provider"
disabled={!isMember && (!hasProjects || !canPerformAnyCreateAction)}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={t(value.i18n_title)}
optionsClassName="max-w-48 sm:max-w-[532px]"
<SelectionDropdown
menuButton={t(value.i18n_title)}
menuButtonWrapperClassName="nodedc-settings-select px-4 py-3 text-13 font-medium"
dropdownContentClassName="max-w-48 sm:max-w-[532px]"
placement="bottom-end"
buttonClassName="py-2 text-13"
>
{EXPORTERS_LIST.map((service) => (
<CustomSelect.Option key={service.provider} className="flex items-center gap-2" value={service}>
<span className="truncate">{t(service.i18n_title)}</span>
</CustomSelect.Option>
))}
</CustomSelect>
options={EXPORTERS_LIST.map((service) => ({
key: service.provider,
title: <span className="truncate">{t(service.i18n_title)}</span>,
isChecked: value.provider === service.provider,
onClick: () => onChange(service),
}))}
/>
)}
/>
}
/>
<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")}
</Button>
</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 gap-2">
<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" : ""}`} />
{refreshing ? t("refreshing") : t("refresh_status")}
</Button>
@ -76,8 +76,9 @@ export const PrevExports = observer(function PrevExports(props: Props) {
{!!exporterServices?.results?.length && (
<div className="flex items-center gap-2 text-11">
<Button
variant="secondary"
size="sm"
variant="ghost"
size="lg"
className="nodedc-settings-chip"
disabled={!exporterServices?.prev_page_results}
onClick={() => exporterServices?.prev_page_results && setCursor(exporterServices?.prev_cursor)}
prependIcon={<MoveLeft />}
@ -85,8 +86,9 @@ export const PrevExports = observer(function PrevExports(props: Props) {
{t("prev")}
</Button>
<Button
variant="secondary"
size="sm"
variant="ghost"
size="lg"
className="nodedc-settings-chip"
disabled={!exporterServices?.next_page_results}
onClick={() => exporterServices?.next_page_results && setCursor(exporterServices?.next_cursor)}
appendIcon={<MoveRight />}
@ -100,7 +102,7 @@ export const PrevExports = observer(function PrevExports(props: Props) {
{exporterServices && exporterServices?.results ? (
exporterServices?.results?.length > 0 ? (
<div>
<div className="divide-y divide-subtle-1">
<div className="nodedc-settings-card overflow-hidden px-1 py-1">
<Table
columns={columns}
data={exporterServices?.results ?? []}

View File

@ -41,7 +41,7 @@ export const StartOfWeekPreference = observer(function StartOfWeekPreference(pro
<SelectionDropdown
placement="bottom-end"
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) => ({
key: `${day.value}`,
title: day.label,

View File

@ -81,7 +81,7 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro
projectId={projectId}
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="flex items-center gap-2">
<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 ? (
<MembersSettingsLoader />
) : (
<div className="divide-y divide-subtle overflow-scroll">
<div className="nodedc-settings-card overflow-scroll px-1 py-1">
{searchedProjectMembers.length !== 0 && (
<ProjectMemberListItem
memberDetails={memberDetails ?? []}

View File

@ -61,11 +61,11 @@ export const ActivityProfileSettingsList = observer(function ProfileActivityList
return (
<>
{userProfileActivity ? (
<ul>
<ul className="space-y-2">
{userProfileActivity.results.map((activityItem: any) => {
if (activityItem.field === "comment")
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 px-1">
{activityItem.field ? (
@ -90,12 +90,12 @@ export const ActivityProfileSettingsList = observer(function ProfileActivityList
</div>
<div className="min-w-0 flex-1">
<div>
<div className="text-11">
<div className="text-11 text-primary">
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name + " Bot"
: activityItem.actor_detail.display_name}
</div>
<p className="mt-0.5 text-11 text-secondary">
<p className="mt-0.5 text-11 text-tertiary">
Commented {calculateTimeAgo(activityItem.created_at)}
</p>
</div>
@ -122,7 +122,7 @@ export const ActivityProfileSettingsList = observer(function ProfileActivityList
if ("field" in activityItem && activityItem.field !== "updated_by")
return (
<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>
@ -153,7 +153,7 @@ export const ActivityProfileSettingsList = observer(function ProfileActivityList
</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">
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
<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")}
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 && (
<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")}
</Button>
</div>

View File

@ -41,20 +41,23 @@ export const APITokensProfileSettings = observer(function APITokensProfileSettin
title={t("account_settings.api_tokens.heading")}
description={t("account_settings.api_tokens.description")}
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")}
</Button>
}
/>
<div className="mt-7">
{tokens.length > 0 ? (
<>
<div>
{tokens.map((token) => (
<ApiTokenListItem key={token.id} token={token} />
))}
</div>
</>
<div className="space-y-2">
{tokens.map((token) => (
<ApiTokenListItem key={token.id} token={token} />
))}
</div>
) : (
<EmptyStateCompact
assetKey="token"

View File

@ -211,63 +211,61 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
/>
<form onSubmit={handleSubmit(onSubmit)} className="w-full">
<div className="flex w-full flex-col gap-7">
<div className="relative h-44 w-full">
<CoverImage
src={userCover}
className="h-44 w-full rounded-lg"
alt={currentUser?.first_name ?? "Cover image"}
/>
<div className="absolute -bottom-6 left-6 flex items-end justify-between">
<div className="flex gap-3">
<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 className="nodedc-settings-card overflow-hidden">
<div className="relative h-44 w-full overflow-hidden rounded-[1.35rem]">
<CoverImage
src={userCover}
className="h-44 w-full rounded-[1.35rem]"
alt={currentUser?.first_name ?? "Cover image"}
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/10 via-transparent to-black/45" />
<div className="absolute right-4 bottom-4 flex">
<Controller
control={control}
name="cover_image_url"
render={({ field: { value, onChange } }) => (
<ImagePickerPopover
label={t("change_cover")}
control={control}
onChange={(imageUrl) => onChange(imageUrl)}
value={value}
isProfileCover
buttonClassName="nodedc-overlay-button min-w-[10.5rem]"
/>
)}
/>
</div>
</div>
<div className="-mt-7 flex items-end justify-between gap-4 px-6 pb-6">
<div className="flex items-end gap-4">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)} className="shrink-0">
{!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 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 className="absolute right-3 bottom-3 flex">
<Controller
control={control}
name="cover_image_url"
render={({ field: { value, onChange } }) => (
<ImagePickerPopover
label={t("change_cover")}
control={control}
onChange={(imageUrl) => onChange(imageUrl)}
value={value}
isProfileCover
/>
)}
/>
</div>
</div>
<div className="item-center mt-6 flex justify-between">
<div className="flex flex-col">
<div className="item-center flex text-16 font-medium text-secondary">
<span>{`${watch("first_name")} ${watch("last_name")}`}</span>
</div>
<span className="text-13 tracking-tight text-tertiary">{watch("email")}</span>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2 xl:grid-cols-3">
<div className="flex flex-col gap-1">
<h4 className="text-13 font-medium text-secondary">
<div className="nodedc-settings-card flex flex-col gap-6 px-5 py-5">
<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;
<span className="text-danger-primary">*</span>
</h4>
@ -288,7 +286,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
ref={ref}
hasError={Boolean(errors.first_name)}
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}
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>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 font-medium text-secondary">{t("last_name")}</h4>
<div className="flex flex-col gap-1.5">
<h4 className="text-13 font-medium text-tertiary">{t("last_name")}</h4>
<Controller
control={control}
name="last_name"
@ -314,7 +312,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
ref={ref}
hasError={Boolean(errors.last_name)}
placeholder={t("profile_general.last_name_placeholder")}
className="w-full rounded-md"
className="nodedc-settings-input w-full"
maxLength={50}
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>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 font-medium text-secondary">
<div className="flex flex-col gap-1.5">
<h4 className="text-13 font-medium text-tertiary">
{t("display_name")}&nbsp;
<span className="text-danger-primary">*</span>
</h4>
@ -344,7 +342,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
ref={ref}
hasError={Boolean(errors?.display_name)}
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}
/>
)}
@ -353,8 +351,8 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
<span className="text-11 text-danger-primary">{errors?.display_name?.message}</span>
)}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 font-medium text-secondary">
<div className="flex flex-col gap-1.5 xl:col-span-2">
<h4 className="text-13 font-medium text-tertiary">
{t("auth.common.email.label")}&nbsp;
<span className="text-danger-primary">*</span>
</h4>
@ -373,7 +371,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
ref={ref}
hasError={Boolean(errors.email)}
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" : ""
}`}
autoComplete="on"
@ -384,7 +382,7 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
{isSMTPConfigured && (
<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)}
>
{t("account_settings.profile.change_email_modal.title")}
@ -393,8 +391,15 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
</div>
</div>
</div>
<div>
<Button variant="primary" type="submit" loading={isLoading}>
<div className="flex items-center justify-between py-1">
<div />
<Button
variant="ghost"
size="lg"
type="submit"
loading={isLoading}
className="nodedc-settings-save-button min-w-[12rem]"
>
{isLoading ? t("saving") : t("save_changes")}
</Button>
</div>
@ -405,7 +410,11 @@ export const GeneralProfileSettingsForm = observer(function GeneralProfileSettin
title={t("deactivate_account")}
description={t("deactivate_account_description")}
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")}
</Button>
}

View File

@ -59,7 +59,7 @@ export const NotificationsProfileSettingsForm = observer(function NotificationsP
}, [reset, data]);
return (
<div className="flex flex-col gap-y-1">
<div className="flex flex-col gap-y-2">
<SettingsControlItem
title={t("property_changes")}
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
title={t("issue_completed")}
description={t("issue_completed_description")}

View File

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

View File

@ -73,7 +73,14 @@ export const ProfileSettingsLanguageAndTimezonePreferencesList = observer(
<SettingsControlItem
title={t("timezone")}
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
title={t("language")}
@ -87,7 +94,7 @@ export const ProfileSettingsLanguageAndTimezonePreferencesList = observer(
onClick: () => handleLanguageChange(item.value),
}))}
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"
/>
}

View File

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

View File

@ -132,10 +132,10 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
<div className="size-full">
<ProfileSettingsHeading title={t("auth.common.password.change_password.label.default")} />
<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 && (
<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">
<Controller
control={control}
@ -150,7 +150,7 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
value={value}
onChange={onChange}
placeholder={t("old_password")}
className="w-full"
className="nodedc-settings-input w-full"
hasError={Boolean(errors.old_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="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">
<Controller
control={control}
@ -190,7 +190,7 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
value={value}
placeholder={t("auth.common.password.new_password.placeholder")}
onChange={onChange}
className="w-full"
className="nodedc-settings-input w-full"
hasError={Boolean(errors.new_password)}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
@ -221,7 +221,7 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
)}
</div>
<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">
<Controller
control={control}
@ -236,7 +236,7 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
placeholder={t("auth.common.password.confirm_password.placeholder")}
value={value}
onChange={onChange}
className="w-full"
className="nodedc-settings-input w-full"
hasError={Boolean(errors.confirm_password)}
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
@ -262,7 +262,14 @@ export const SecurityProfileSettings = observer(function SecurityProfileSettings
</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
? `${t("auth.common.password.change_password.label.submitting")}`
: t("auth.common.password.change_password.label.default")}

View File

@ -39,19 +39,25 @@ export const ProfileSettingsModal = observer(function ProfileSettingsModal() {
handleClose={handleClose}
position={EModalPosition.CENTER}
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">
<ProfileSettingsSidebarRoot
activeTab={activeTab}
className="w-[250px] rounded-l-xl"
className="w-[280px] rounded-l-[1.85rem]"
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 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>
</ModalCore>

View File

@ -105,7 +105,12 @@ export function CreateWebhookModal(props: ICreateWebhookModal) {
});
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 ? (
<WebhookForm onSubmit={handleCreateWebhook} handleClose={handleClose} />
) : (

View File

@ -17,17 +17,15 @@ type Props = {
export function WebhooksEmptyState(props: Props) {
const { onClick } = props;
return (
<div
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`}
>
<div className="flex w-full flex-col items-center text-center">
<img src={EmptyWebhook} className="w-52 object-cover sm:w-60" alt="empty" />
<h6 className="mt-6 mb-3 text-18 font-semibold sm:mt-8">No webhooks</h6>
<p className="mb-7 text-tertiary sm:mb-8">Create webhooks to receive real-time updates and automate actions</p>
<Button className="flex items-center gap-1.5" onClick={onClick}>
<div className="mx-auto flex w-full max-w-[34rem] flex-col items-center text-center">
<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>
<p className="mt-3 max-w-[28rem] text-body-sm-regular text-tertiary">
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}>
Add webhook
</Button>
</div>
</Button>
</div>
);
}

View File

@ -69,7 +69,7 @@ export const WebhookForm = observer(function WebhookForm(props: Props) {
return (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-5">
<div className="text-18 font-medium text-secondary">
<div className="text-18 font-medium text-primary">
{data
? t("workspace_settings.settings.webhooks.modal.details")
: 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">
<WebhookSecretKey data={data} />
<Button
variant="ghost"
size="lg"
type="submit"
loading={isSubmitting}
className="nodedc-settings-save-button min-w-[11rem]"
data-ph-element={WORKSPACE_SETTINGS_TRACKER_ELEMENTS.WEBHOOK_UPDATE_BUTTON}
>
{isSubmitting ? t("updating") : t("update")}
@ -109,11 +111,17 @@ export const WebhookForm = observer(function WebhookForm(props: Props) {
</div>
) : (
<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")}
</Button>
{!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")}
</Button>
)}

View File

@ -29,12 +29,12 @@ export function WebhooksListItem(props: IWebhookListItem) {
};
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
href={`/${workspaceSlug}/settings/webhooks/${webhook?.id}`}
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">
<ToggleSwitch value={webhook.is_active} onChange={handleToggle} />
</div>

View File

@ -15,7 +15,7 @@ export const WebhooksList = observer(function WebhooksList() {
const { webhooks } = useWebhook();
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) => (
<WebhooksListItem key={webhook.id} webhook={webhook} />
))}

View File

@ -122,17 +122,17 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
}}
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">
<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]}
</span>
<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 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>
</div>
<SelectionDropdown
@ -167,7 +167,7 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
},
}))}
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
className={`flex items-center rounded-sm text-caption-sm-medium ${
hasRoleChangeAccess ? "" : "text-placeholder"

View File

@ -91,7 +91,7 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
if (isEmpty(columns)) return <MembersLayoutLoader />;
return (
<div className="grid border-t border-subtle">
<div className="grid overflow-hidden rounded-[1.2rem]">
{removeMemberModal && (
<ConfirmWorkspaceMemberRemove
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[]
}
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"
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"
/>
</div>

View File

@ -72,7 +72,7 @@ export const WorkspaceMembersList = observer(function WorkspaceMembersList(props
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 ?? []} />}
{searchedInvitationsIds?.length === 0 && searchedMemberIds?.length === 0 && (
<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"
className=""
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">
<h4 className="pt-2 pb-2 text-h5-medium">{t("workspace_settings.settings.members.pending_invites")}</h4>
{searchedInvitationsIds && (
@ -97,7 +97,7 @@ export const WorkspaceMembersList = observer(function WorkspaceMembersList(props
}
>
<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) => (
<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="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">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)} disabled={!isAdmin}>
{workspaceLogo && workspaceLogo !== "" ? (
<div className="relative flex size-14">
<img
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"
/>
</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"}
</div>
)}
</button>
</div>
<div className="flex flex-col gap-1">
<div className="mb:-my-5 text-h5-semibold leading-6">{watch("name")}</div>
<button type="button" onClick={handleCopyUrl} className="text-left text-body-xs-regular tracking-tight">{`${
<div className="flex flex-col gap-1.5">
<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 text-tertiary">{`${
typeof window !== "undefined" && window.location.origin.replace("http://", "").replace("https://", "")
}/${currentWorkspace.slug}`}</button>
{isAdmin && (
@ -188,9 +188,9 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
)}
</div>
</div>
<div className="flex flex-col gap-7">
<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="flex flex-col gap-2">
<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-8 xl:grid-cols-2 2xl:grid-cols-3">
<div className="flex flex-col gap-2.5">
<h4 className="text-body-sm-medium text-tertiary">{t("workspace_settings.settings.general.name")}</h4>
<Controller
control={control}
@ -208,14 +208,14 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
ref={ref}
hasError={Boolean(errors.name)}
placeholder={t("workspace_settings.settings.general.name")}
className="w-full rounded-md"
className="nodedc-settings-input w-full"
disabled={!isAdmin}
/>
)}
/>
{errors.name && <p className="text-caption-sm-regular text-danger-primary">{errors.name.message}</p>}
</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.company_size")}
</h4>
@ -234,13 +234,13 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
ORGANIZATION_SIZE.find((c) => c === value) ??
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}
/>
)}
/>
</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>
<Controller
control={control}
@ -257,13 +257,13 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
onChange={onChange}
ref={ref}
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
/>
)}
/>
</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.workspace_timezone")}
</h4>
@ -280,10 +280,11 @@ export const WorkspaceDetails = observer(function WorkspaceDetails() {
</div>
</div>
{isAdmin && (
<div className="flex items-center justify-between py-2">
<div className="flex items-center justify-between py-1">
<Button
variant="primary"
size="lg"
className="nodedc-settings-save-button min-w-[13rem]"
onClick={(e) => {
void handleSubmit(onSubmit)(e);
}}