UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: настройки проекта, модалки и drag-and-drop канон

This commit is contained in:
DCCONSTRUCTIONS 2026-04-20 09:22:37 +03:00
parent bd7a7d96fe
commit df68c96795
47 changed files with 703 additions and 275 deletions

111
HDESIGN-CODE.md Normal file
View File

@ -0,0 +1,111 @@
# HDESIGN CODE
Документ фиксирует канон интерфейса NODE.DC, чтобы не обсуждать одни и те же правила повторно.
## Источник цветов
- Основной runtime-конфиг цветов: [design.config.json](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/design.config.json)
- Рабочая web-копия: [plane-src/apps/web/design.config.json](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/apps/web/design.config.json)
- В рантайме используются CSS variables:
- `--nodedc-accent-rgb`
- `--nodedc-card-passive-rgb`
- `--nodedc-card-active-rgb`
## Цветовые правила
- `accent_rgb`: акцентный цвет интерфейса.
- `passive_card_rgb`: пассивные карточки.
- `active_card_rgb`: активные карточки и primary CTA в зелёной теме.
- Primary/active элементы используют акцентный или `active_card_rgb`.
- Secondary элементы не должны иметь ярких outline и цветных рамок без явной причины.
## Радиусы
- Главные модалки и большие surface-контейнеры: `1.75rem`
- Стандартные glass-карточки и settings-карточки: `1.35rem`
- Поля ввода, селекты, secondary/primary кнопки, chip-кнопки: `1.25rem`
- Малые круглые action-кнопки: `999px` или полный круг при квадратной коробке
## Outline и рамки
- Внешние outline у контролов запрещены.
- Синий browser outline должен быть снят и заменён на нормальный hover/focus surface.
- Если нужен контур, он должен быть частью дизайна:
- мягкий glass border
- акцентный border для drag/drop или active-state
- Красные технические outline, debug-рамки и случайные browser-box shadow запрещены.
## Glass и фон
- Popup, dropdown, modal, sidebar overlays и settings-карточки используют matte black glass.
- База:
- тёмный полупрозрачный фон
- `backdrop-filter: blur(...)`
- мягкая стеклянная граница
- Popup не должны выглядеть просто прозрачными. Если blur не читается, проблема в слое рендера, а не в одном `rgba`.
## Кнопки
- Все кнопки без жёсткого outline.
- Primary button:
- фон: акцентный или `active_card_rgb`
- текст: чёрный или очень тёмный, если фон светлый
- hover: более светлая версия того же цвета
- Save/update button:
- если это зафиксированный green CTA, текст должен быть контрастным и читаемым
- hover осветляет текущий тон, а не уходит в синий
- Secondary button:
- тёмный glass фон
- без border-outline
- hover немного светлее базового surface
- Danger button:
- без кислотно-красных рамок
- мягкий danger surface
## Поля и селекты
- Все поля ввода, textarea, select, chip-select:
- скруглённые
- без внешних outline
- glass background
- единая вертикальная высота для одного класса контролов
- Placeholder и label должны быть читаемы и не прилипать к краям.
## Toolbar и верхние панели
- Элементы верхней панели центрируются по одной горизонтальной оси.
- Активный layout/tool mode выделяется кругом акцентного цвета, не квадратной плашкой.
- Кнопки `Отображение`, `Аналитика`, `Добавить рабочий элемент`:
- одинаковая высота
- каноничные радиусы
- нормальные горизонтальные paddings, чтобы текст не лип к краям
## Карточки
- Внутренние карточки строятся по симметричным верхним и нижним padding.
- Верхняя ось:
- аватар
- имя
- вторичная строка
- action-circle справа
должны сидеть на согласованной геометрии
- Нижняя ось:
- assignee bubbles
- дата
должны быть симметричны верхней
## Dropdown и popup
- Все dropdown/popup приводятся к единому matte glass канону.
- Запрещены:
- квадратные active-box вокруг круглых кнопок
- жёсткие border-outline
- светлый фон, если основной экран тёмный
- Search shell внутри popup должен использовать тот же стиль, что и сам popup.
## Drag and drop
- Drag overlay использует акцентный контур.
- Delete dropzone:
- без красного технического свечения
- текст локализован
- акцентный outline допустим
## Тексты
- Пользовательский UI на русском, если экран русифицирован.
- Не оставлять смешанные подписи вида `Created at / Updated at / Label / State group`, если экран уже на русском.
## Правило внедрения
- Новый экран или popup не стилизуется локально “на глаз”.
- Сначала используется существующий shared-класс или shared-component.
- Если shared-слоя нет, создаётся reusable-класс/компонент и уже через него приводятся все похожие места.
- Цель: не точечная покраска одного окна, а единый системный канон.

View File

@ -1,7 +0,0 @@
{
"nodedc": {
"accent_rgb": [195, 255, 102],
"passive_card_rgb": [42, 43, 46],
"active_card_rgb": [195, 255, 102]
}
}

1
design.config.json Symbolic link
View File

@ -0,0 +1 @@
plane-src/apps/web/design.config.json

View File

@ -4,7 +4,7 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import type { ReactNode } from "react"; import type { CSSProperties, ReactNode } from "react";
import Script from "next/script"; import Script from "next/script";
import { Links, Meta, Outlet, Scripts } from "react-router"; import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router"; import type { LinksFunction } from "react-router";
@ -22,6 +22,7 @@ import icon512 from "@/app/assets/icons/icon-512x512.png?url";
import ogImage from "@/app/assets/og-image.png?url"; import ogImage from "@/app/assets/og-image.png?url";
import globalStyles from "@/styles/globals.css?url"; import globalStyles from "@/styles/globals.css?url";
import type { Route } from "./+types/root"; import type { Route } from "./+types/root";
import designConfig from "../design.config.json";
// components // components
import { LogoSpinner } from "@/components/common/logo-spinner"; import { LogoSpinner } from "@/components/common/logo-spinner";
// local // local
@ -35,6 +36,12 @@ import "@fontsource/ibm-plex-mono";
const APP_TITLE = "NODE.DC | Self-hosted task management workspace."; const APP_TITLE = "NODE.DC | Self-hosted task management workspace.";
const designConfigStyle = {
"--nodedc-accent-rgb": designConfig.nodedc.accent_rgb.join(" "),
"--nodedc-card-passive-rgb": designConfig.nodedc.passive_card_rgb.join(" "),
"--nodedc-card-active-rgb": designConfig.nodedc.active_card_rgb.join(" "),
} as CSSProperties;
export const links: LinksFunction = () => [ export const links: LinksFunction = () => [
{ rel: "icon", type: "image/png", sizes: "32x32", href: favicon32 }, { rel: "icon", type: "image/png", sizes: "32x32", href: favicon32 },
{ rel: "icon", type: "image/png", sizes: "16x16", href: favicon16 }, { rel: "icon", type: "image/png", sizes: "16x16", href: favicon16 },
@ -58,7 +65,7 @@ export function Layout({ children }: { children: ReactNode }) {
const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0"); const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning style={designConfigStyle}>
<head> <head>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />

View File

@ -24,7 +24,7 @@ export const EstimateListItemButtons = observer(function EstimateListItemButtons
return ( return (
<div className="relative flex items-center gap-1"> <div className="relative flex items-center gap-1">
<button <button
className="relative flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-colors hover:bg-layer-1" className="nodedc-settings-secondary-button relative flex h-10 w-10 min-h-0 min-w-0 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-[1rem] px-0 transition-colors"
onClick={() => onDeleteClick && onDeleteClick(estimateId)} onClick={() => onDeleteClick && onDeleteClick(estimateId)}
data-ph-element={PROJECT_SETTINGS_TRACKER_ELEMENTS.ESTIMATES_LIST_ITEM} data-ph-element={PROJECT_SETTINGS_TRACKER_ELEMENTS.ESTIMATES_LIST_ITEM}
> >

View File

@ -120,7 +120,7 @@ export const IssuesHeader = observer(function IssuesHeader() {
<Button <Button
variant="primary" variant="primary"
size="lg" size="lg"
className="nodedc-toolbar-primary" className="nodedc-toolbar-primary nodedc-toolbar-primary-wide"
onClick={() => { onClick={() => {
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}} }}

View File

@ -24,6 +24,8 @@ function ProjectAttributes(props: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { control } = useFormContext<IProject>(); const { control } = useFormContext<IProject>();
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile); const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile);
const projectAttributeChipClassName =
"nodedc-modal-chip !h-10 !rounded-[1.25rem] !px-4 !py-2 !text-13";
return ( return (
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Controller <Controller
@ -50,8 +52,8 @@ function ProjectAttributes(props: Props) {
</div> </div>
} }
placement="bottom-start" placement="bottom-start"
className="h-full" className="h-10"
buttonClassName="h-full" buttonClassName={projectAttributeChipClassName}
noChevron noChevron
tabIndex={getIndex("network")} tabIndex={getIndex("network")}
> >
@ -84,6 +86,9 @@ function ProjectAttributes(props: Props) {
placeholder={t("lead")} placeholder={t("lead")}
multiple={false} multiple={false}
buttonVariant="border-with-text" buttonVariant="border-with-text"
buttonClassName={projectAttributeChipClassName}
buttonContainerClassName="!h-10"
showUserDetails
tabIndex={getIndex("lead")} tabIndex={getIndex("lead")}
/> />
</div> </div>

View File

@ -167,8 +167,8 @@ export const CreateProjectForm = observer(function CreateProjectForm(props: TCre
<FormProvider {...methods}> <FormProvider {...methods}>
<ProjectCreateHeader handleClose={handleClose} isMobile={isMobile} /> <ProjectCreateHeader handleClose={handleClose} isMobile={isMobile} />
<form onSubmit={handleSubmit(onSubmit)} className="px-3"> <form onSubmit={handleSubmit(onSubmit)} className="px-6">
<div className="mt-9 space-y-6 pb-5"> <div className="mt-10 space-y-6 pb-6">
<ProjectCommonAttributes <ProjectCommonAttributes
setValue={setValue} setValue={setValue}
isMobile={isMobile} isMobile={isMobile}

View File

@ -21,6 +21,7 @@ import { Button, getButtonStyling } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types"; import { EFileAssetType } from "@plane/types";
import { Input, Loader } from "@plane/ui"; import { Input, Loader } from "@plane/ui";
import { cn } from "@plane/utils";
// helpers // helpers
import { STATIC_COVER_IMAGES, getCoverImageDisplayURL } from "@/helpers/cover-image.helper"; import { STATIC_COVER_IMAGES, getCoverImageDisplayURL } from "@/helpers/cover-image.helper";
// hooks // hooks
@ -40,6 +41,7 @@ type Props = {
value: string | null; value: string | null;
control: Control<any>; control: Control<any>;
onChange: (data: string) => void; onChange: (data: string) => void;
buttonClassName?: string;
disabled?: boolean; disabled?: boolean;
tabIndex?: number; tabIndex?: number;
isProfileCover?: boolean; isProfileCover?: boolean;
@ -50,7 +52,17 @@ type Props = {
const fileService = new FileService(); const fileService = new FileService();
export const ImagePickerPopover = observer(function ImagePickerPopover(props: Props) { export const ImagePickerPopover = observer(function ImagePickerPopover(props: Props) {
const { label, value, control, onChange, disabled = false, tabIndex, isProfileCover = false, projectId } = props; const {
label,
value,
control,
onChange,
buttonClassName = "",
disabled = false,
tabIndex,
isProfileCover = false,
projectId,
} = props;
const { t } = useTranslation(); const { t } = useTranslation();
// states // states
const [image, setImage] = useState<File | null>(null); const [image, setImage] = useState<File | null>(null);
@ -191,7 +203,11 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
return ( return (
<Popover className="relative z-19" ref={ref} tabIndex={tabIndex} onKeyDown={handleKeyDown}> <Popover className="relative z-19" ref={ref} tabIndex={tabIndex} onKeyDown={handleKeyDown}>
<Popover.Button className={getButtonStyling("secondary", "sm")} onClick={handleOnClick} disabled={disabled}> <Popover.Button
className={cn(getButtonStyling("secondary", "sm"), buttonClassName)}
onClick={handleOnClick}
disabled={disabled}
>
{label} {label}
</Popover.Button> </Popover.Button>

View File

@ -9,6 +9,7 @@ import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
// components // components
import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item"; import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item";
import { SettingsHeading } from "@/components/settings/heading"; import { SettingsHeading } from "@/components/settings/heading";
@ -74,7 +75,7 @@ export const EstimateRoot = observer(function EstimateRoot(props: TEstimateRoot)
/> />
{/* active estimates section */} {/* active estimates section */}
<div className="mt-12 flex flex-col gap-y-4"> <div className="mt-12 flex flex-col gap-y-4">
<SettingsHeading title="Estimates list" variant="h6" /> <SettingsHeading title={t("project_settings.estimates.list_heading")} variant="h6" />
<EstimateList <EstimateList
estimateIds={[currentActiveEstimateId]} estimateIds={[currentActiveEstimateId]}
isAdmin={isAdmin} isAdmin={isAdmin}
@ -91,12 +92,15 @@ export const EstimateRoot = observer(function EstimateRoot(props: TEstimateRoot)
assetClassName="size-20" assetClassName="size-20"
title={t("settings_empty_state.estimates.title")} title={t("settings_empty_state.estimates.title")}
description={t("settings_empty_state.estimates.description")} description={t("settings_empty_state.estimates.description")}
actions={[ customButton={
{ <Button
label: t("settings_empty_state.estimates.cta_primary"), variant="ghost"
onClick: () => setIsEstimateCreateModalOpen(true), className="nodedc-settings-primary-button"
}, onClick={() => setIsEstimateCreateModalOpen(true)}
]} >
{t("settings_empty_state.estimates.cta_primary")}
</Button>
}
align="start" align="start"
rootClassName="py-20" rootClassName="py-20"
/> />
@ -105,11 +109,10 @@ export const EstimateRoot = observer(function EstimateRoot(props: TEstimateRoot)
{archivedEstimateIds && archivedEstimateIds.length > 0 && ( {archivedEstimateIds && archivedEstimateIds.length > 0 && (
<div className="mt-12 flex flex-col gap-y-4"> <div className="mt-12 flex flex-col gap-y-4">
<SettingsHeading <SettingsHeading
title="Archived estimates" title={t("project_settings.estimates.archived_heading")}
description={ description={
<> <>
Estimates have gone through a change, these are the estimates you had in your older versions which {t("project_settings.estimates.archived_description")}&nbsp;
were not in use. Read more about them&nbsp;
<a <a
href={"https://docs.plane.so/core-concepts/projects/run-project#estimate"} href={"https://docs.plane.so/core-concepts/projects/run-project#estimate"}
target="_blank" target="_blank"

View File

@ -74,16 +74,16 @@ export const DeleteInboxIssueModal = observer(function DeleteInboxIssueModal({
isSubmitting={isDeleting} isSubmitting={isDeleting}
isOpen={isOpen} isOpen={isOpen}
title={t("inbox_issue.modals.delete.title")} title={t("inbox_issue.modals.delete.title")}
// TODO: Need to translate the confirmation message
content={ content={
<> t("inbox_issue.modals.delete.content", {
Are you sure you want to delete work item{" "} value: `${projectDetails?.identifier}-${data?.sequence_id}`,
<span className="font-medium break-words text-primary"> })
{projectDetails?.identifier}-{data?.sequence_id}
</span>
{""}? The work item will only be deleted from the intake and this action cannot be undone.
</>
} }
primaryButtonText={{
loading: t("deleting"),
default: t("delete"),
}}
secondaryButtonText={t("cancel")}
/> />
); );
}); });

View File

@ -119,14 +119,17 @@ export const DeleteIssueModal = observer(function DeleteIssueModal(props: Props)
title={t("entity.delete.label", { entity: isEpic ? t("common.epic") : t("common.work_item") })} title={t("entity.delete.label", { entity: isEpic ? t("common.epic") : t("common.work_item") })}
content={ content={
<> <>
{/* TODO: Translate here */} {t("entity.delete.confirmation", {
{`Are you sure you want to delete ${isEpic ? "epic" : "work item"} `} entity: isEpic ? t("common.epic") : t("common.work_item"),
<span className="font-medium break-words text-primary"> identifier: `${projectDetails?.identifier}-${issue?.sequence_id}`,
{projectDetails?.identifier}-{issue?.sequence_id} })}
</span>
{` ? All of the data related to the ${isEpic ? "epic" : "work item"} will be permanently removed. This action cannot be undone.`}
</> </>
} }
primaryButtonText={{
loading: t("deleting"),
default: t("delete"),
}}
secondaryButtonText={t("cancel")}
/> />
); );
}); });

View File

@ -128,7 +128,7 @@ export const HeaderFilters = observer(function HeaderFilters(props: Props) {
</FiltersDropdown> </FiltersDropdown>
{canUserCreateIssue ? ( {canUserCreateIssue ? (
<Button <Button
className="nodedc-toolbar-pill hidden md:inline-flex" className="nodedc-toolbar-pill nodedc-toolbar-pill-wide hidden md:inline-flex"
onClick={() => setAnalyticsModal(true)} onClick={() => setAnalyticsModal(true)}
variant="secondary" variant="secondary"
size="lg" size="lg"

View File

@ -61,7 +61,7 @@ export function FiltersDropdown(props: Props) {
variant="secondary" variant="secondary"
prependIcon={icon} prependIcon={icon}
tabIndex={tabIndex} tabIndex={tabIndex}
className="nodedc-toolbar-pill relative" className="nodedc-toolbar-pill nodedc-toolbar-pill-wide relative"
data-active={open} data-active={open}
size="lg" size="lg"
> >
@ -81,7 +81,7 @@ export function FiltersDropdown(props: Props) {
ref={setReferenceElement} ref={setReferenceElement}
variant="secondary" variant="secondary"
tabIndex={tabIndex} tabIndex={tabIndex}
className="nodedc-toolbar-pill" className="nodedc-toolbar-pill nodedc-toolbar-pill-wide"
size="lg" size="lg"
> >
{miniIcon || title} {miniIcon || title}

View File

@ -51,10 +51,10 @@ export function GroupDragOverlay(props: Props) {
<div <div
ref={messageContainerRef} ref={messageContainerRef}
className={cn( className={cn(
`absolute top-0 left-0 h-full w-full items-center rounded-sm bg-layer-1/85 text-13 font-medium text-tertiary ${dragColumnOrientation}`, `absolute top-0 left-0 h-full w-full items-center rounded-[1.5rem] bg-layer-1/85 text-13 font-medium text-tertiary ${dragColumnOrientation}`,
{ {
"z-2 flex flex-col border-[1px] border-strong": shouldOverlayBeVisible, "z-2 flex flex-col border border-[rgb(var(--nodedc-accent-rgb))]/55 bg-black/35": shouldOverlayBeVisible,
"bg-danger-subtle": workflowDisabledSource && isDropDisabled, "bg-black/40": workflowDisabledSource && isDropDisabled,
}, },
{ hidden: !shouldOverlayBeVisible } { hidden: !shouldOverlayBeVisible }
)} )}
@ -67,14 +67,13 @@ export function GroupDragOverlay(props: Props) {
/> />
) : ( ) : (
<div <div
className={cn("my-8 flex flex-col items-center rounded-sm p-3", { className={cn(
"text-secondary": shouldOverlayBeVisible, "my-8 flex max-w-[20rem] flex-col items-center rounded-[1.25rem] px-4 py-3 text-center text-secondary"
"text-danger-secondary": isDropDisabled, )}
})}
> >
{dropErrorMessage ? ( {dropErrorMessage ? (
<div className="flex items-center"> <div className="flex items-center">
<AlertCircle width={13} height={13} /> &nbsp; <AlertCircle width={13} height={13} className="text-[rgb(var(--nodedc-accent-rgb))]" /> &nbsp;
<span>{dropErrorMessage}</span> <span>{dropErrorMessage}</span>
</div> </div>
) : ( ) : (
@ -84,7 +83,7 @@ export function GroupDragOverlay(props: Props) {
{t("issue.layouts.ordered_by_label")} <span className="font-semibold">{t(readableOrderBy)}</span>. {t("issue.layouts.ordered_by_label")} <span className="font-semibold">{t(readableOrderBy)}</span>.
</span> </span>
)} )}
<span>{t("entity.drop_here_to_move", { entity: isEpic ? "epic" : "work item" })}</span> <span>{t("entity.drop_here_to_move", { entity: t(isEpic ? "common.epic" : "common.work_item") })}</span>
</> </>
)} )}
</div> </div>

View File

@ -12,6 +12,7 @@ import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-sc
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { EIssueFilterType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EIssueFilterType, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import type { EIssuesStoreType } from "@plane/types"; import type { EIssuesStoreType } from "@plane/types";
import { EIssueServiceType, EIssueLayoutTypes } from "@plane/types"; import { EIssueServiceType, EIssueLayoutTypes } from "@plane/types";
//hooks //hooks
@ -89,6 +90,7 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa
const [isDragOverDelete, setIsDragOverDelete] = useState(false); const [isDragOverDelete, setIsDragOverDelete] = useState(false);
const { isDragging } = useKanbanView(); const { isDragging } = useKanbanView();
const { t } = useTranslation();
const displayFilters = issuesFilter?.issueFilters?.displayFilters; const displayFilters = issuesFilter?.issueFilters?.displayFilters;
const displayProperties = issuesFilter?.issueFilters?.displayProperties; const displayProperties = issuesFilter?.issueFilters?.displayProperties;
@ -249,17 +251,17 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa
<div <div
className={`fixed left-1/2 -translate-x-1/2 ${ className={`fixed left-1/2 -translate-x-1/2 ${
isDragging ? "z-40" : "" isDragging ? "z-40" : ""
} top-3 mx-3 flex w-72 items-center justify-center`} } top-3 mx-3 flex w-[min(40rem,calc(100vw-2rem))] items-center justify-center`}
ref={deleteAreaRef} ref={deleteAreaRef}
> >
<div <div
className={`${ className={`${
isDragging ? `opacity-100` : `opacity-0` isDragging ? `opacity-100` : `opacity-0`
} flex w-full items-center justify-center rounded-sm border-2 border-danger-strong/20 bg-surface-1 px-3 py-5 text-11 font-medium text-danger-primary italic ${ } nodedc-glass-popup-surface flex w-full items-center justify-center rounded-[1.5rem] border-2 border-[rgb(var(--nodedc-accent-rgb))]/80 px-6 py-4 text-center text-[13px] font-medium text-secondary ring-1 ring-[rgb(var(--nodedc-accent-rgb))]/20 ${
isDragOverDelete ? "bg-danger-primary blur-2xl" : "" isDragOverDelete ? "border-[rgb(var(--nodedc-accent-rgb))] bg-white/6 ring-[rgb(var(--nodedc-accent-rgb))]/35" : ""
} transition duration-300`} } transition duration-300`}
> >
Drop here to delete the work item. {t("entity.drop_here_to_delete", { entity: t("common.work_item") })}
</div> </div>
</div> </div>
<IssueLayoutHOC layout={EIssueLayoutTypes.KANBAN}> <IssueLayoutHOC layout={EIssueLayoutTypes.KANBAN}>

View File

@ -415,9 +415,9 @@ export const CreateUpdateIssueModalBase = observer(function CreateUpdateIssueMod
return ( return (
<ModalCore <ModalCore
isOpen={isOpen} isOpen={isOpen}
position={EModalPosition.TOP} position={EModalPosition.CENTER}
width={isDuplicateModalOpen ? EModalWidth.VIXL : EModalWidth.XXXXL} width={isDuplicateModalOpen ? EModalWidth.VIXL : EModalWidth.XXXXL}
className="rounded-lg !bg-transparent shadow-none transition-[width] ease-linear" className="transition-[width] ease-linear"
> >
{withDraftIssueWrapper ? ( {withDraftIssueWrapper ? (
<DraftIssueLayout {...commonIssueModalProps} changesMade={changesMade} onChange={handleFormChange} /> <DraftIssueLayout {...commonIssueModalProps} changesMade={changesMade} onChange={handleFormChange} />

View File

@ -154,7 +154,7 @@ export const IssueDescriptionEditor = observer(function IssueDescriptionEditor(p
}; };
return ( return (
<div className="relative rounded-lg border-[0.5px] border-subtle-1 bg-layer-2"> <div className="nodedc-modal-editor relative min-h-[180px]">
{descriptionHtmlData === undefined || !projectId ? ( {descriptionHtmlData === undefined || !projectId ? (
<Loader className="max-h-64 min-h-[120px] space-y-2 overflow-hidden rounded-md border border-subtle p-3 py-2 pt-3"> <Loader className="max-h-64 min-h-[120px] space-y-2 overflow-hidden rounded-md border border-subtle p-3 py-2 pt-3">
<Loader.Item width="100%" height="26px" /> <Loader.Item width="100%" height="26px" />
@ -203,7 +203,7 @@ export const IssueDescriptionEditor = observer(function IssueDescriptionEditor(p
project_id: projectId?.toString() ?? "", project_id: projectId?.toString() ?? "",
}) })
} }
containerClassName="pt-3 min-h-[120px]" containerClassName="min-h-[180px] !border-none !bg-transparent !p-0"
uploadFile={async (blockId, file) => { uploadFile={async (blockId, file) => {
try { try {
const { asset_id } = await uploadEditorAsset({ const { asset_id } = await uploadEditorAsset({
@ -243,11 +243,11 @@ export const IssueDescriptionEditor = observer(function IssueDescriptionEditor(p
/> />
)} )}
/> />
<div className="border-0.5 z-10 flex items-center justify-end gap-2 p-3"> <div className="z-10 flex items-center justify-end gap-2 px-4 pb-4">
{issueName && issueName.trim() !== "" && config?.has_llm_configured && ( {issueName && issueName.trim() !== "" && config?.has_llm_configured && (
<button <button
type="button" type="button"
className={`flex items-center gap-1 rounded-sm bg-surface-2 px-1.5 py-1 text-caption-sm-regular hover:bg-layer-1 ${ className={`flex items-center gap-1 rounded-[1rem] bg-white/6 px-2.5 py-1.5 text-caption-sm-regular text-secondary transition-colors hover:bg-white/10 ${
iAmFeelingLucky ? "cursor-wait" : "" iAmFeelingLucky ? "cursor-wait" : ""
}`} }`}
onClick={handleAutoGenerateDescription} onClick={handleAutoGenerateDescription}

View File

@ -72,7 +72,7 @@ export const IssueTitleInput = observer(function IssueTitleInput(props: TIssueTi
ref={issueTitleRef || ref} ref={issueTitleRef || ref}
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
placeholder={t("title")} placeholder={t("title")}
className="w-full text-body-sm-regular" className="nodedc-modal-input w-full !px-4 !py-3 !text-[15px]"
autoFocus autoFocus
tabIndex={getIndex("name")} tabIndex={getIndex("name")}
/> />

View File

@ -376,15 +376,15 @@ export const IssueFormRoot = observer(function IssueFormRoot(props: IssueFormPro
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<div className="flex gap-2 bg-transparent"> <div className="nodedc-work-item-modal flex gap-3 bg-transparent">
<div className="w-full rounded-lg"> <div className="w-full">
<form <form
ref={formRef} ref={formRef}
onSubmit={handleSubmit((data) => handleFormSubmit(data))} onSubmit={handleSubmit((data) => handleFormSubmit(data))}
className="flex w-full flex-col" className="flex w-full flex-col"
> >
<div className="rounded-t-lg bg-surface-1 p-5"> <div className="px-6 pt-6 pb-2">
<h3 className="pb-2 text-h4-medium text-secondary">{modalTitle}</h3> <h3 className="pb-2 text-18 font-medium text-secondary">{modalTitle}</h3>
<div className="flex items-center justify-between pt-2 pb-4"> <div className="flex items-center justify-between pt-2 pb-4">
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
<IssueProjectSelect <IssueProjectSelect
@ -452,7 +452,7 @@ export const IssueFormRoot = observer(function IssueFormRoot(props: IssueFormPro
</div> </div>
<div <div
className={cn( className={cn(
"space-y-3 bg-surface-1 pb-4", "space-y-4 bg-transparent pb-4",
activeAdditionalPropertiesLength > 4 && activeAdditionalPropertiesLength > 4 &&
"vertical-scrollbar scrollbar-sm max-h-[45vh] overflow-hidden overflow-y-auto" "vertical-scrollbar scrollbar-sm max-h-[45vh] overflow-hidden overflow-y-auto"
)} )}
@ -486,12 +486,7 @@ export const IssueFormRoot = observer(function IssueFormRoot(props: IssueFormPro
workspaceSlug={workspaceSlug?.toString()} workspaceSlug={workspaceSlug?.toString()}
/> />
</div> </div>
<div <div className={cn("border-t border-subtle/70 bg-transparent px-6 py-4")}>
className={cn(
"rounded-b-lg border-t-[0.5px] border-subtle bg-surface-1 px-4 py-3",
activeAdditionalPropertiesLength > 0 && "shadow-raised-100"
)}
>
<div className="pb-3"> <div className="pb-3">
<IssueDefaultProperties <IssueDefaultProperties
control={control} control={control}
@ -509,7 +504,7 @@ export const IssueFormRoot = observer(function IssueFormRoot(props: IssueFormPro
</div> </div>
{showActionButtons && ( {showActionButtons && (
<div <div
className="flex items-center justify-end gap-4 border-t-[0.5px] border-subtle pt-6 pb-3" className="flex items-center justify-end gap-4 border-t border-subtle/70 pt-6 pb-2"
tabIndex={getIndex("create_more")} tabIndex={getIndex("create_more")}
> >
{!data?.id && ( {!data?.id && (
@ -530,6 +525,7 @@ export const IssueFormRoot = observer(function IssueFormRoot(props: IssueFormPro
<Button <Button
variant="secondary" variant="secondary"
size="lg" size="lg"
className="nodedc-modal-secondary-button min-w-[8.25rem]"
onClick={() => { onClick={() => {
if (editorRef.current?.isEditorReadyToDiscard()) { if (editorRef.current?.isEditorReadyToDiscard()) {
onClose(); onClose();
@ -553,6 +549,10 @@ export const IssueFormRoot = observer(function IssueFormRoot(props: IssueFormPro
ref={submitBtnRef} ref={submitBtnRef}
loading={isSubmitting} loading={isSubmitting}
disabled={isDisabled} disabled={isDisabled}
className={cn(
"min-w-[8.25rem]",
moveToIssue ? "nodedc-modal-secondary-button" : "nodedc-modal-primary-button"
)}
> >
{isSubmitting ? primaryButtonText.loading : primaryButtonText.default} {isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
</Button> </Button>
@ -566,6 +566,7 @@ export const IssueFormRoot = observer(function IssueFormRoot(props: IssueFormPro
onClick={handleMoveToProjects} onClick={handleMoveToProjects}
disabled={isMoving} disabled={isMoving}
size="lg" size="lg"
className="nodedc-modal-primary-button min-w-[8.25rem]"
> >
{t("add_to_project")} {t("add_to_project")}
</Button> </Button>
@ -579,7 +580,7 @@ export const IssueFormRoot = observer(function IssueFormRoot(props: IssueFormPro
{shouldRenderDuplicateModal && ( {shouldRenderDuplicateModal && (
<div <div
ref={modalContainerRef} ref={modalContainerRef}
className="shadow-xl bg-pi-50 relative flex flex-col gap-2.5 rounded-lg px-3 py-4" className="nodedc-glass-surface relative flex flex-col gap-2.5 rounded-[1.75rem] px-4 py-4"
style={{ maxHeight: formRef?.current?.offsetHeight ? `${formRef.current.offsetHeight}px` : "436px" }} style={{ maxHeight: formRef?.current?.offsetHeight ? `${formRef.current.offsetHeight}px` : "436px" }}
> >
<DuplicateModalRoot <DuplicateModalRoot

View File

@ -161,7 +161,7 @@ export const CreateUpdateLabelInline = observer(
<> <>
<div <div
ref={ref} ref={ref}
className={`flex w-full scroll-m-8 items-center gap-2 bg-surface-1 ${labelForm ? "" : "hidden"}`} className={`flex w-full scroll-m-8 items-center gap-2 ${labelForm ? "" : "hidden"}`}
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Popover className="relative z-10 flex h-full w-full items-center justify-center"> <Popover className="relative z-10 flex h-full w-full items-center justify-center">
@ -229,21 +229,22 @@ export const CreateUpdateLabelInline = observer(
ref={ref} ref={ref}
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
placeholder={t("project_settings.labels.label_title")} placeholder={t("project_settings.labels.label_title")}
className="w-full" className="nodedc-settings-input w-full"
/> />
)} )}
/> />
</div> </div>
<Button variant="secondary" onClick={() => handleClose()}> <Button variant="ghost" onClick={() => handleClose()} className="nodedc-settings-secondary-button">
{t("cancel")} {t("cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="ghost"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
handleSubmit(handleFormSubmit)(); handleSubmit(handleFormSubmit)();
}} }}
loading={isSubmitting} loading={isSubmitting}
className="nodedc-settings-primary-button"
> >
{isUpdating ? (isSubmitting ? t("updating") : t("update")) : isSubmitting ? t("adding") : t("add")} {isUpdating ? (isSubmitting ? t("updating") : t("update")) : isSubmitting ? t("adding") : t("add")}
</Button> </Button>

View File

@ -33,7 +33,7 @@ export function LabelDragPreview(props: LabelDragPreviewProps) {
const { label, isGroup } = props; const { label, isGroup } = props;
return ( return (
<div className="border-[1px] border-subtle bg-surface-1 py-3 pr-4 pl-2"> <div className="nodedc-settings-card rounded-[1.25rem] py-3 pr-4 pl-2">
<LabelName name={label.name} color={label.color} isGroup={isGroup} /> <LabelName name={label.name} color={label.color} isGroup={isGroup} />
</div> </div>
); );

View File

@ -78,13 +78,11 @@ export const ProjectSettingLabelGroup = observer(function ProjectSettingLabelGro
<LabelDndHOC label={label} isGroup isChild={false} isLastChild={isLastChild} onDrop={onDrop}> <LabelDndHOC label={label} isGroup isChild={false} isLastChild={isLastChild} onDrop={onDrop}>
{(isDragging, isDroppingInLabel, dragHandleRef) => ( {(isDragging, isDroppingInLabel, dragHandleRef) => (
<div <div
className={`rounded-sm ${isDroppingInLabel ? "border-[2px] border-accent-strong" : "border-[1.5px] border-transparent"}`} className={`rounded-[1.25rem] ${isDroppingInLabel ? "border-[2px] border-accent-strong" : "border-[1.5px] border-transparent"}`}
> >
<Disclosure <Disclosure
as="div" as="div"
className={`rounded-sm text-primary ${ className={`nodedc-settings-card rounded-[1.25rem] text-primary ${isDragging ? "bg-layer-1" : "bg-surface-1"}`}
!isDroppingInLabel ? "border-[0.5px] border-subtle" : ""
} ${isDragging ? "bg-layer-1" : "bg-surface-1"}`}
defaultOpen defaultOpen
> >
{({ open }) => ( {({ open }) => (

View File

@ -87,12 +87,12 @@ export function ProjectSettingLabelItem(props: Props) {
<LabelDndHOC label={label} isGroup={false} isChild={isChild} isLastChild={isLastChild} onDrop={onDrop}> <LabelDndHOC label={label} isGroup={false} isChild={isChild} isLastChild={isLastChild} onDrop={onDrop}>
{(isDragging, isDroppingInLabel, dragHandleRef) => ( {(isDragging, isDroppingInLabel, dragHandleRef) => (
<div <div
className={`rounded-sm ${isDroppingInLabel ? "border-[2px] border-accent-strong" : "border-[1.5px] border-transparent"}`} className={`rounded-[1.25rem] ${isDroppingInLabel ? "border-[2px] border-accent-strong" : "border-[1.5px] border-transparent"}`}
> >
<div <div
className={`group relative flex items-center justify-between gap-2 space-y-3 rounded-sm px-1 py-3 ${ className={`nodedc-settings-card group relative flex items-center justify-between gap-2 space-y-3 rounded-[1.25rem] px-3 py-3 ${
isDroppingInLabel ? "" : "border-[0.5px] border-subtle" isDragging || isParentDragging ? "bg-layer-1" : "bg-surface-1"
} ${isDragging || isParentDragging ? "bg-layer-1" : "bg-surface-1"}`} }`}
> >
{isEditLabelForm ? ( {isEditLabelForm ? (
<CreateUpdateLabelInline <CreateUpdateLabelInline

View File

@ -85,7 +85,7 @@ export const ProjectSettingsLabelList = observer(function ProjectSettingsLabelLi
description={t("project_settings.labels.description")} description={t("project_settings.labels.description")}
control={ control={
isEditable && ( isEditable && (
<Button variant="primary" size="lg" onClick={newLabel}> <Button variant="ghost" size="lg" onClick={newLabel} className="nodedc-settings-primary-button min-w-[11rem]">
{t("common.add_label")} {t("common.add_label")}
</Button> </Button>
) )
@ -93,7 +93,7 @@ export const ProjectSettingsLabelList = observer(function ProjectSettingsLabelLi
/> />
<div className="mt-6 w-full"> <div className="mt-6 w-full">
{showLabelForm && ( {showLabelForm && (
<div className="my-2 w-full rounded-sm border border-subtle px-3.5 py-2"> <div className="nodedc-settings-card my-2 w-full px-4 py-3">
<CreateUpdateLabelInline <CreateUpdateLabelInline
labelForm={showLabelForm} labelForm={showLabelForm}
setLabelForm={setLabelForm} setLabelForm={setLabelForm}
@ -114,14 +114,11 @@ export const ProjectSettingsLabelList = observer(function ProjectSettingsLabelLi
assetClassName="size-20" assetClassName="size-20"
title={t("settings_empty_state.labels.title")} title={t("settings_empty_state.labels.title")}
description={t("settings_empty_state.labels.description")} description={t("settings_empty_state.labels.description")}
actions={[ customButton={
{ <Button variant="ghost" className="nodedc-settings-primary-button" onClick={newLabel}>
label: t("settings_empty_state.labels.cta_primary"), {t("settings_empty_state.labels.cta_primary")}
onClick: () => { </Button>
newLabel(); }
},
},
]}
align="start" align="start"
rootClassName="py-20" rootClassName="py-20"
/> />

View File

@ -22,7 +22,7 @@ type TStateForm = {
function PopoverButton({ color }: { color?: string }) { function PopoverButton({ color }: { color?: string }) {
return ( return (
<div <div
className="group inline-flex h-5 w-5 items-center rounded-sm text-14 font-medium transition-all focus:outline-none" className="group inline-flex h-5 w-5 items-center rounded-full text-14 font-medium transition-all focus:outline-none"
style={{ style={{
backgroundColor: color ?? "black", backgroundColor: color ?? "black",
}} }}
@ -65,7 +65,7 @@ export function StateForm(props: TStateForm) {
}; };
return ( return (
<div className="relative flex space-x-2 rounded-sm bg-surface-1 p-3"> <div className="nodedc-settings-card relative flex space-x-2 rounded-[1.25rem] p-3">
{/* color */} {/* color */}
<div className="mt-2 h-full flex-shrink-0"> <div className="mt-2 h-full flex-shrink-0">
<Popover button={<PopoverButton color={formData?.color} />} panelClassName="mt-4 -ml-3"> <Popover button={<PopoverButton color={formData?.color} />} panelClassName="mt-4 -ml-3">
@ -83,7 +83,7 @@ export function StateForm(props: TStateForm) {
value={formData?.name} value={formData?.name}
onChange={(e) => handleFormData("name", e.target.value)} onChange={(e) => handleFormData("name", e.target.value)}
hasError={(errors && Boolean(errors.name)) || false} hasError={(errors && Boolean(errors.name)) || false}
className="w-full" className="nodedc-settings-input w-full"
maxLength={100} maxLength={100}
autoFocus autoFocus
/> />
@ -96,14 +96,27 @@ export function StateForm(props: TStateForm) {
value={formData?.description} value={formData?.description}
onChange={(e) => handleFormData("description", e.target.value)} onChange={(e) => handleFormData("description", e.target.value)}
hasError={(errors && Boolean(errors.description)) || false} hasError={(errors && Boolean(errors.description)) || false}
className="min-h-14 w-full resize-none text-13" className="nodedc-settings-input min-h-14 w-full resize-none text-13"
/> />
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button onClick={formSubmit} variant="primary" size="lg" disabled={buttonDisabled}> <Button
onClick={formSubmit}
variant="ghost"
size="lg"
disabled={buttonDisabled}
className="nodedc-settings-primary-button"
>
{buttonTitle} {buttonTitle}
</Button> </Button>
<Button type="button" variant="secondary" size="lg" disabled={buttonDisabled} onClick={onCancel}> <Button
type="button"
variant="ghost"
size="lg"
disabled={buttonDisabled}
onClick={onCancel}
className="nodedc-settings-secondary-button"
>
{t("cancel")} {t("cancel")}
</Button> </Button>
</div> </div>

View File

@ -57,7 +57,7 @@ export const GroupItem = observer(function GroupItem(props: TGroupItem) {
return ( return (
<div <div
className={cn("space-y-1 rounded-sm border border-subtle bg-surface-2 p-2 transition-all", groupItemClassName)} className={cn("nodedc-settings-card space-y-1 rounded-[1.35rem] p-3 transition-all", groupItemClassName)}
ref={dropElementRef} ref={dropElementRef}
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@ -68,6 +68,7 @@ export const GroupItem = observer(function GroupItem(props: TGroupItem) {
<div <div
className={cn( className={cn(
"flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-sm transition-all", "flex h-5 w-5 flex-shrink-0 items-center justify-center overflow-hidden rounded-sm transition-all",
"rounded-full",
{ {
"rotate-0": currentStateExpanded, "rotate-0": currentStateExpanded,
"-rotate-90": !currentStateExpanded, "-rotate-90": !currentStateExpanded,
@ -76,7 +77,7 @@ export const GroupItem = observer(function GroupItem(props: TGroupItem) {
> >
<ChevronDownIcon className="h-4 w-4" /> <ChevronDownIcon className="h-4 w-4" />
</div> </div>
<div className="flex h-6 w-6 flex-shrink-0 items-center justify-center overflow-hidden rounded-sm"> <div className="flex h-6 w-6 flex-shrink-0 items-center justify-center overflow-hidden rounded-full">
<StateGroupIcon stateGroup={groupKey} size={EIconSize.XL} /> <StateGroupIcon stateGroup={groupKey} size={EIconSize.XL} />
</div> </div>
<div className="px-1 text-14 font-medium text-secondary">{translatedGroupKey}</div> <div className="px-1 text-14 font-medium text-secondary">{translatedGroupKey}</div>
@ -86,6 +87,7 @@ export const GroupItem = observer(function GroupItem(props: TGroupItem) {
data-ph-element={STATE_TRACKER_ELEMENTS.STATE_GROUP_ADD_BUTTON} data-ph-element={STATE_TRACKER_ELEMENTS.STATE_GROUP_ADD_BUTTON}
className={cn( className={cn(
"flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm text-accent-primary/80 transition-colors hover:bg-layer-1 hover:text-accent-primary", "flex h-6 w-6 flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm text-accent-primary/80 transition-colors hover:bg-layer-1 hover:text-accent-primary",
"rounded-full hover:bg-white/6",
(!isEditable || createState) && "cursor-not-allowed text-placeholder hover:text-placeholder" (!isEditable || createState) && "cursor-not-allowed text-placeholder hover:text-placeholder"
)} )}
onClick={() => { onClick={() => {

View File

@ -135,7 +135,7 @@ export const StateItem = observer(function StateItem(props: TStateItem) {
<div <div
ref={draggableElementRef} ref={draggableElementRef}
className={cn( className={cn(
"group relative rounded-sm border border-subtle bg-surface-1 px-3.5 py-3", "nodedc-settings-field group relative rounded-[1.2rem] px-3.5 py-3",
isDragging ? `opacity-50` : `opacity-100`, isDragging ? `opacity-50` : `opacity-100`,
totalStates === 1 ? `cursor-auto` : `cursor-grab`, totalStates === 1 ? `cursor-auto` : `cursor-grab`,
stateItemClassName stateItemClassName

View File

@ -66,7 +66,12 @@ export function CreateProjectModal(props: Props) {
}); });
return ( return (
<ModalCore isOpen={isOpen} position={EModalPosition.TOP} width={EModalWidth.XXXXL}> <ModalCore
isOpen={isOpen}
position={EModalPosition.CENTER}
width={EModalWidth.XXXXL}
className="overflow-hidden"
>
{currentStep === EProjectCreationSteps.CREATE_PROJECT && ( {currentStep === EProjectCreationSteps.CREATE_PROJECT && (
<CreateProjectForm <CreateProjectForm
setToFavorite={setToFavorite} setToFavorite={setToFavorite}

View File

@ -79,7 +79,7 @@ function ProjectCommonAttributes(props: Props) {
onChange={handleNameChange(onChange)} onChange={handleNameChange(onChange)}
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
placeholder={t("project_name")} placeholder={t("project_name")}
className="focus:border-blue-400 w-full" className="nodedc-modal-input w-full text-13"
tabIndex={getIndex("name")} tabIndex={getIndex("name")}
/> />
)} )}
@ -113,7 +113,7 @@ function ProjectCommonAttributes(props: Props) {
onChange={handleIdentifierChange(onChange)} onChange={handleIdentifierChange(onChange)}
hasError={Boolean(errors.identifier)} hasError={Boolean(errors.identifier)}
placeholder={t("project_id")} placeholder={t("project_id")}
className={cn("focus:border-blue-400 w-full pr-7 text-11", { className={cn("nodedc-modal-input w-full pr-7 text-12", {
uppercase: value, uppercase: value,
})} })}
tabIndex={getIndex("identifier")} tabIndex={getIndex("identifier")}
@ -144,7 +144,7 @@ function ProjectCommonAttributes(props: Props) {
onChange(e); onChange(e);
handleFormOnChange?.(); handleFormOnChange?.();
}} }}
className="focus:border-blue-400 !h-24 text-13" className="nodedc-modal-input !h-24 text-13"
hasError={Boolean(errors?.description)} hasError={Boolean(errors?.description)}
tabIndex={getIndex("description")} tabIndex={getIndex("description")}
/> />

View File

@ -48,11 +48,11 @@ function ProjectCreateHeader(props: Props) {
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile); const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile);
return ( return (
<div className="group relative h-44 w-full rounded-lg"> <div className="group relative h-44 w-full rounded-[1.75rem]">
<CoverImage <CoverImage
src={coverImage} src={coverImage}
alt={t("project_cover_image_alt")} alt={t("project_cover_image_alt")}
className="absolute top-0 left-0 h-full w-full rounded-lg" className="absolute top-0 left-0 h-full w-full rounded-[1.75rem]"
/> />
{showActionButtons && ( {showActionButtons && (
<div className="absolute top-2.5 left-2.5"> <div className="absolute top-2.5 left-2.5">
@ -60,8 +60,13 @@ function ProjectCreateHeader(props: Props) {
</div> </div>
)} )}
{isClosable && ( {isClosable && (
<div className="absolute top-2 right-2 p-2"> <div className="absolute top-3 right-3">
<button type="button" onClick={handleClose} tabIndex={getIndex("close")}> <button
type="button"
onClick={handleClose}
tabIndex={getIndex("close")}
className="grid h-10 w-10 place-items-center rounded-[1.25rem] bg-black/32 text-on-color outline-none backdrop-blur-md transition-colors hover:bg-black/48"
>
<CloseIcon className="h-5 w-5 text-on-color" /> <CloseIcon className="h-5 w-5 text-on-color" />
</button> </button>
</div> </div>
@ -79,6 +84,7 @@ function ProjectCreateHeader(props: Props) {
}} }}
control={control} control={control}
value={value ?? null} value={value ?? null}
buttonClassName="nodedc-overlay-button"
tabIndex={getIndex("cover_image")} tabIndex={getIndex("cover_image")}
/> />
)} )}
@ -96,7 +102,7 @@ function ProjectCreateHeader(props: Props) {
className="flex items-center justify-center" className="flex items-center justify-center"
buttonClassName="flex items-center justify-center" buttonClassName="flex items-center justify-center"
label={ label={
<span className="grid h-11 w-11 place-items-center rounded-md border border-subtle bg-layer-2"> <span className="grid h-11 w-11 place-items-center rounded-[1.2rem] bg-black/36 shadow-[0_10px_24px_rgba(0,0,0,0.22)] backdrop-blur-md">
<Logo logo={value} size={20} /> <Logo logo={value} size={20} />
</span> </span>
} }

View File

@ -30,10 +30,23 @@ function ProjectCreateButtons(props: Props) {
return ( return (
<div className="flex justify-end gap-2 border-t border-subtle py-4"> <div className="flex justify-end gap-2 border-t border-subtle py-4">
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={getIndex("cancel")}> <Button
variant="ghost"
size="lg"
onClick={handleClose}
tabIndex={getIndex("cancel")}
className="nodedc-modal-secondary-button min-w-[8.75rem]"
>
{t("common.cancel")} {t("common.cancel")}
</Button> </Button>
<Button variant="primary" size="lg" type="submit" loading={isSubmitting} tabIndex={getIndex("submit")}> <Button
variant="ghost"
size="lg"
type="submit"
loading={isSubmitting}
tabIndex={getIndex("submit")}
className="nodedc-modal-primary-button min-w-[10.5rem]"
>
{isSubmitting ? t("creating") : t("create_project")} {isSubmitting ? t("creating") : t("create_project")}
</Button> </Button>
</div> </div>

View File

@ -7,6 +7,7 @@
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { ChevronDownIcon } from "@plane/propel/icons"; import { ChevronDownIcon } from "@plane/propel/icons";
import { EUserProjectRoles, EUserWorkspaceRoles } from "@plane/types"; import { EUserProjectRoles, EUserWorkspaceRoles } from "@plane/types";
@ -52,11 +53,12 @@ const RoleFilterGroup = observer(function RoleFilterGroup({
const [isExpanded, setIsExpanded] = useState(true); const [isExpanded, setIsExpanded] = useState(true);
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
const roleOptions = memberType === "project" ? PROJECT_ROLE_OPTIONS : WORKSPACE_ROLE_OPTIONS; const roleOptions = memberType === "project" ? PROJECT_ROLE_OPTIONS : WORKSPACE_ROLE_OPTIONS;
const { t } = useTranslation();
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<FilterHeader <FilterHeader
title={`Roles${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`} title={`${t("roles")}${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`}
isPreviewEnabled={isExpanded} isPreviewEnabled={isExpanded}
handleIsPreviewEnabled={() => setIsExpanded(!isExpanded)} handleIsPreviewEnabled={() => setIsExpanded(!isExpanded)}
/> />
@ -96,13 +98,14 @@ export const MemberListFiltersDropdown = observer(function MemberListFiltersDrop
const { appliedFilters, handleUpdate, memberType } = props; const { appliedFilters, handleUpdate, memberType } = props;
const appliedFiltersCount = appliedFilters?.length ?? 0; const appliedFiltersCount = appliedFilters?.length ?? 0;
const { t } = useTranslation();
return ( return (
<CustomMenu <CustomMenu
customButton={ customButton={
<div className="relative"> <div className="relative">
<Button variant="secondary" size="lg" className="flex items-center gap-2"> <Button variant="ghost" size="lg" className="nodedc-settings-secondary-button flex items-center gap-2">
<span>Filters</span> <span>{t("filters")}</span>
<ChevronDownIcon className="h-3 w-3" /> <ChevronDownIcon className="h-3 w-3" />
</Button> </Button>
{appliedFiltersCount > 0 && ( {appliedFiltersCount > 0 && (

View File

@ -255,6 +255,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
control={control} control={control}
onChange={onChange} onChange={onChange}
value={value ?? null} value={value ?? null}
buttonClassName="nodedc-overlay-button"
disabled={!isAdmin} disabled={!isAdmin}
projectId={project.id} projectId={project.id}
/> />
@ -286,7 +287,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
value={value} value={value}
onChange={onChange} onChange={onChange}
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
className="rounded-md !p-3 font-medium" className="nodedc-settings-input !px-4 !py-3 font-medium"
placeholder={t("common.project_name")} placeholder={t("common.project_name")}
disabled={!isAdmin} disabled={!isAdmin}
/> />
@ -306,7 +307,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
value={value} value={value}
placeholder={t("project_description_placeholder")} placeholder={t("project_description_placeholder")}
onChange={onChange} onChange={onChange}
className="min-h-[102px] text-13 font-medium" className="nodedc-settings-input min-h-[102px] !rounded-[1.35rem] text-13 font-medium"
hasError={Boolean(errors?.description)} hasError={Boolean(errors?.description)}
disabled={!isAdmin} disabled={!isAdmin}
/> />
@ -342,7 +343,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
ref={ref} ref={ref}
hasError={Boolean(errors.identifier)} hasError={Boolean(errors.identifier)}
placeholder={t("project_settings.general.enter_project_id")} placeholder={t("project_settings.general.enter_project_id")}
className="w-full font-medium" className="nodedc-settings-input w-full pr-10 font-medium"
disabled={!isAdmin} disabled={!isAdmin}
/> />
)} )}
@ -383,7 +384,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
)} )}
</div> </div>
} }
buttonClassName="!border-subtle !shadow-none font-medium rounded-md" buttonClassName="nodedc-settings-select !h-12 font-medium"
input input
disabled={!isAdmin} disabled={!isAdmin}
// optionsClassName="w-full" // optionsClassName="w-full"
@ -418,7 +419,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
onChange(value); onChange(value);
}} }}
error={Boolean(errors.timezone)} error={Boolean(errors.timezone)}
buttonClassName="border-none" buttonClassName="nodedc-settings-select !h-12 !border-0"
disabled={!isAdmin} disabled={!isAdmin}
/> />
</> </>
@ -429,7 +430,14 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
</div> </div>
<div className="flex items-center justify-between py-2"> <div className="flex items-center justify-between py-2">
<> <>
<Button variant="primary" size="lg" type="submit" loading={isLoading} disabled={!isAdmin}> <Button
variant="ghost"
size="lg"
type="submit"
loading={isLoading}
disabled={!isAdmin}
className="nodedc-settings-save-button min-w-[11.5rem]"
>
{isLoading ? t("updating") : t("common.update_project")} {isLoading ? t("updating") : t("common.update_project")}
</Button> </Button>
<span className="text-13 text-placeholder italic"> <span className="text-13 text-placeholder italic">

View File

@ -84,11 +84,11 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro
<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 border-b border-subtle py-2">
<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="flex items-center justify-start gap-1.5 rounded-md border border-subtle bg-surface-1 px-2 py-1"> <div className="nodedc-settings-field flex min-h-[2.75rem] items-center justify-start gap-1.5 px-3">
<SearchIcon className="h-3.5 w-3.5" /> <SearchIcon className="h-3.5 w-3.5" />
<input <input
className="w-full max-w-[234px] border-none bg-transparent text-13 placeholder:text-placeholder focus:outline-none" className="w-full max-w-[234px] border-none bg-transparent text-13 placeholder:text-placeholder focus:outline-none"
placeholder="Search" placeholder={t("search")}
value={searchQuery} value={searchQuery}
autoFocus autoFocus
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
@ -101,11 +101,12 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro
/> />
{isAdmin && ( {isAdmin && (
<Button <Button
variant="primary" variant="ghost"
onClick={() => { onClick={() => {
setInviteModal(true); setInviteModal(true);
}} }}
data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON} data-ph-element={MEMBER_TRACKER_ELEMENTS.HEADER_ADD_BUTTON}
className="nodedc-settings-primary-button min-w-[11rem]"
> >
{t("add_member")} {t("add_member")}
</Button> </Button>

View File

@ -8,6 +8,7 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Ban } from "lucide-react"; import { Ban } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { EUserProjectRoles } from "@plane/types"; import { EUserProjectRoles } from "@plane/types";
// plane ui // plane ui
import { Avatar, CustomSearchSelect } from "@plane/ui"; import { Avatar, CustomSearchSelect } from "@plane/ui";
@ -24,6 +25,7 @@ type Props = {
export const MemberSelect = observer(function MemberSelect(props: Props) { export const MemberSelect = observer(function MemberSelect(props: Props) {
const { value, onChange, isDisabled = false } = props; const { value, onChange, isDisabled = false } = props;
const { t } = useTranslation();
// router // router
const { projectId } = useParams(); const { projectId } = useParams();
// store hooks // store hooks
@ -72,12 +74,12 @@ export const MemberSelect = observer(function MemberSelect(props: Props) {
) : ( ) : (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Ban className="h-3.5 w-3.5 rotate-90 text-placeholder" /> <Ban className="h-3.5 w-3.5 rotate-90 text-placeholder" />
<span className="text-13 text-placeholder">None</span> <span className="text-13 text-placeholder">{t("none")}</span>
</div> </div>
)} )}
</div> </div>
} }
buttonClassName="!px-3 !py-2 bg-surface-1" buttonClassName="nodedc-settings-select !w-full !justify-between !px-4 !py-3"
options={ options={
options && options &&
options && [ options && [
@ -88,7 +90,7 @@ export const MemberSelect = observer(function MemberSelect(props: Props) {
content: ( content: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Ban className="h-3.5 w-3.5 rotate-90 text-placeholder" /> <Ban className="h-3.5 w-3.5 rotate-90 text-placeholder" />
<span className="py-0.5 text-13 text-placeholder">None</span> <span className="py-0.5 text-13 text-placeholder">{t("none")}</span>
</div> </div>
), ),
}, },

View File

@ -8,7 +8,7 @@ import { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// ui // ui
import { Button, getButtonStyling } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { Logo } from "@plane/propel/emoji-icon-picker"; import { Logo } from "@plane/propel/emoji-icon-picker";
import { Row } from "@plane/ui"; import { Row } from "@plane/ui";
// components // components
@ -35,7 +35,7 @@ export const ProjectFeatureUpdate = observer(function ProjectFeatureUpdate(props
return ( return (
<> <>
<Row className="py-6"> <Row className="px-6 py-6">
<ProjectFeaturesList workspaceSlug={workspaceSlug} projectId={projectId} isAdmin /> <ProjectFeaturesList workspaceSlug={workspaceSlug} projectId={projectId} isAdmin />
</Row> </Row>
<div className="mt-4 flex items-center justify-between gap-2 border-t border-subtle px-6 py-4"> <div className="mt-4 flex items-center justify-between gap-2 border-t border-subtle px-6 py-4">
@ -45,13 +45,19 @@ export const ProjectFeatureUpdate = observer(function ProjectFeatureUpdate(props
{t("created").toLowerCase()}. {t("created").toLowerCase()}.
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="secondary" size="lg" onClick={onClose} tabIndex={1}> <Button
variant="ghost"
size="lg"
onClick={onClose}
tabIndex={1}
className="nodedc-modal-secondary-button min-w-[8.75rem]"
>
{t("close")} {t("close")}
</Button> </Button>
<Link <Link
href={`/${workspaceSlug}/projects/${projectId}/issues`} href={`/${workspaceSlug}/projects/${projectId}/issues`}
onClick={onClose} onClick={onClose}
className={getButtonStyling("primary", "lg")} className="nodedc-modal-primary-button inline-flex min-w-[10.5rem] items-center justify-center"
tabIndex={2} tabIndex={2}
> >
{t("open_project")} {t("open_project")}

View File

@ -36,9 +36,9 @@ type TDefaultSettingItemProps = {
function DefaultSettingItem({ title, description, children }: TDefaultSettingItemProps) { function DefaultSettingItem({ title, description, children }: TDefaultSettingItemProps) {
return ( return (
<div className="flex items-center justify-between gap-x-2"> <div className="nodedc-settings-card flex items-center justify-between gap-x-6 px-5 py-4">
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-1">
<h4 className="text-13 font-medium">{title}</h4> <h4 className="text-13 font-medium text-primary">{title}</h4>
<p className="text-11 text-tertiary">{description}</p> <p className="text-11 text-tertiary">{description}</p>
</div> </div>
<div className="w-full max-w-48 sm:max-w-64">{children}</div> <div className="w-full max-w-48 sm:max-w-64">{children}</div>
@ -139,7 +139,10 @@ export const ProjectSettingsMemberDefaults = observer(function ProjectSettingsMe
return ( return (
<div className="my-6 flex flex-col gap-y-6"> <div className="my-6 flex flex-col gap-y-6">
<DefaultSettingItem title="Project Lead" description="Select the project lead for the project."> <DefaultSettingItem
title={t("project_settings.members.project_lead")}
description={t("project_settings.members.project_lead_description")}
>
{currentProjectDetails ? ( {currentProjectDetails ? (
<Controller <Controller
control={control} control={control}
@ -160,7 +163,10 @@ export const ProjectSettingsMemberDefaults = observer(function ProjectSettingsMe
</Loader> </Loader>
)} )}
</DefaultSettingItem> </DefaultSettingItem>
<DefaultSettingItem title="Default Assignee" description="Select the default assignee for the project."> <DefaultSettingItem
title={t("project_settings.members.default_assignee")}
description={t("project_settings.members.default_assignee_description")}
>
{currentProjectDetails ? ( {currentProjectDetails ? (
<Controller <Controller
control={control} control={control}
@ -183,8 +189,8 @@ export const ProjectSettingsMemberDefaults = observer(function ProjectSettingsMe
</DefaultSettingItem> </DefaultSettingItem>
{currentProjectDetails && ( {currentProjectDetails && (
<DefaultSettingItem <DefaultSettingItem
title="Guest access" title={t("project_settings.members.guest_super_permissions.title")}
description="This will allow guests to have view access to all the project work items." description={t("project_settings.members.guest_super_permissions.sub_heading")}
> >
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<ToggleSwitch <ToggleSwitch

View File

@ -55,28 +55,29 @@ export const GeneralProjectSettingsControlSection = observer(function GeneralPro
isOpen={Boolean(selectProject)} isOpen={Boolean(selectProject)}
onClose={() => setSelectedProject(null)} onClose={() => setSelectedProject(null)}
/> />
<div className="rounded-lg border border-subtle bg-layer-2"> <div className="flex flex-col gap-1.5">
{/* Project Selector */}
<SettingsBoxedControlItem <SettingsBoxedControlItem
className="rounded-b-none border-0 border-b"
title={t("archive")} title={t("archive")}
description={t("project_settings_control.archive_description")} description={t("project_settings_control.archive_description")}
control={ control={
<Button variant="secondary" onClick={() => setArchiveProject(true)}> <Button
variant="ghost"
onClick={() => setArchiveProject(true)}
className="nodedc-settings-secondary-button"
>
{t("archive")} {t("archive")}
</Button> </Button>
} }
/> />
{/* Format Selector */}
<SettingsBoxedControlItem <SettingsBoxedControlItem
className="rounded-t-none border-0"
title={t("delete")} title={t("delete")}
description={t("project_settings_control.delete_description")} description={t("project_settings_control.delete_description")}
control={ control={
<Button <Button
variant="error-outline" variant="ghost"
onClick={() => setSelectedProject(currentProjectDetails.id ?? null)} onClick={() => setSelectedProject(currentProjectDetails.id ?? null)}
data-ph-element={PROJECT_TRACKER_ELEMENTS.DELETE_PROJECT_BUTTON} data-ph-element={PROJECT_TRACKER_ELEMENTS.DELETE_PROJECT_BUTTON}
className="nodedc-settings-danger-button"
> >
{t("delete")} {t("delete")}
</Button> </Button>

View File

@ -20,7 +20,7 @@ export function SettingsBoxedControlItem(props: Props) {
return ( return (
<div <div
className={cn( className={cn(
"flex w-full flex-col items-start gap-4 rounded-lg border border-subtle bg-layer-2 px-4 py-3 md:flex-row md:items-center md:justify-between md:gap-8", "nodedc-settings-card flex w-full flex-col items-start gap-4 px-5 py-4 md:flex-row md:items-center md:justify-between md:gap-8",
className className
)} )}
> >

View File

@ -0,0 +1,7 @@
{
"nodedc": {
"accent_rgb": [195, 255, 102],
"passive_card_rgb": [42, 43, 46],
"active_card_rgb": [195, 255, 102]
}
}

View File

@ -1,102 +1,33 @@
/** /*
* Copyright 2018 Google Inc. All Rights Reserved. * Temporary cleanup service worker.
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. * We intentionally stop intercepting requests, clear legacy caches, and
* You may obtain a copy of the License at * unregister ourselves. This prevents stale HTML/chunk bundles from surviving
* http://www.apache.org/licenses/LICENSE-2.0 * fresh web deployments and causing client-side hydration/runtime crashes.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
// If the loader is already loaded, just stop. self.addEventListener("install", (event) => {
if (!self.define) { event.waitUntil(self.skipWaiting());
let registry = {}; });
// Used for `eval` and `importScripts` where we can't get script URL by other means. self.addEventListener("activate", (event) => {
// In both cases, it's safe to use a global var because those functions are synchronous. event.waitUntil(
let nextDefineUri; (async () => {
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys.map((cacheKey) => caches.delete(cacheKey)));
const singleRequire = (uri, parentUri) => { const clients = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
uri = new URL(uri + ".js", parentUri).href; await Promise.all(
return ( clients.map((client) =>
registry[uri] || "navigate" in client && typeof client.navigate === "function" ? client.navigate(client.url) : Promise.resolve()
new Promise((resolve) => { )
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
}).then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
}
return promise;
})
); );
};
self.define = (depsNames, factory) => { await self.registration.unregister();
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href; })()
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = (depUri) => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require,
};
registry[uri] = Promise.all(depsNames.map((depName) => specialDeps[depName] || require(depName))).then((deps) => {
factory(...deps);
return exports;
});
};
}
define(["./workbox-9f2f79cf"], function (workbox) {
"use strict";
importScripts();
self.skipWaiting();
workbox.clientsClaim();
workbox.registerRoute(
"/",
new workbox.NetworkFirst({
cacheName: "start-url",
plugins: [
{
cacheWillUpdate: async ({ request, response, event, state }) => {
if (response && response.type === "opaqueredirect") {
return new Response(response.body, {
status: 200,
statusText: "OK",
headers: response.headers,
});
}
return response;
},
},
],
}),
"GET"
);
workbox.registerRoute(
/.*/i,
new workbox.NetworkOnly({
cacheName: "dev",
plugins: [],
}),
"GET"
); );
}); });
//# sourceMappingURL=sw.js.map
self.addEventListener("fetch", () => {
// Intentionally empty: do not cache or intercept runtime requests.
});

View File

@ -30,6 +30,9 @@
--nodedc-accent-rgb: 51 163 255; --nodedc-accent-rgb: 51 163 255;
--nodedc-card-passive-rgb: 42 43 46; --nodedc-card-passive-rgb: 42 43 46;
--nodedc-card-active-rgb: 195 255 102; --nodedc-card-active-rgb: 195 255 102;
--brand-default: rgb(var(--nodedc-accent-rgb));
--brand-300: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 35%, white);
--brand-700: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 75%, black);
/* end background colors */ /* end background colors */
} }
/* background colors */ /* background colors */
@ -419,7 +422,7 @@
box-shadow: none !important; box-shadow: none !important;
border-radius: 999px !important; border-radius: 999px !important;
min-height: 2.5rem; min-height: 2.5rem;
padding-inline: 1rem; padding-inline: 1.35rem;
background: rgba(18, 18, 22, 0.94) !important; background: rgba(18, 18, 22, 0.94) !important;
color: var(--text-color-primary) !important; color: var(--text-color-primary) !important;
transition: transition:
@ -428,6 +431,11 @@
transform 160ms ease; transform 160ms ease;
} }
.nodedc-toolbar-pill-wide {
min-width: 9.75rem;
padding-inline: 1.75rem;
}
.nodedc-toolbar-pill:hover { .nodedc-toolbar-pill:hover {
background: rgba(24, 24, 29, 0.96) !important; background: rgba(24, 24, 29, 0.96) !important;
} }
@ -442,13 +450,261 @@
box-shadow: none !important; box-shadow: none !important;
border-radius: 999px !important; border-radius: 999px !important;
min-height: 2.5rem; min-height: 2.5rem;
padding-inline: 1rem; padding-inline: 1.55rem;
background: rgb(var(--nodedc-accent-rgb)) !important; background: rgb(var(--nodedc-accent-rgb)) !important;
color: #0b1117 !important; color: #0b1117 !important;
} }
.nodedc-toolbar-primary-wide {
min-width: 13rem;
padding-inline: 2rem;
}
.nodedc-toolbar-primary:hover { .nodedc-toolbar-primary:hover {
background: rgba(var(--nodedc-accent-rgb), 0.92) !important; background: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 82%, white) !important;
}
.nodedc-modal-secondary-button {
min-height: 2.75rem;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
background: rgba(255, 255, 255, 0.06) !important;
color: var(--text-color-primary) !important;
padding-inline: 1.25rem !important;
}
.nodedc-modal-secondary-button:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
.nodedc-modal-primary-button {
min-height: 2.75rem;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
background: rgb(var(--nodedc-card-active-rgb)) !important;
color: #0b1117 !important;
padding-inline: 1.25rem !important;
}
.nodedc-modal-primary-button:hover {
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
}
.nodedc-modal-danger-button {
min-height: 2.75rem;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
padding-inline: 1.25rem !important;
}
.nodedc-modal-chip {
min-height: 2.5rem;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
rgba(255, 255, 255, 0.028) !important;
color: var(--text-color-secondary) !important;
padding-inline: 1rem !important;
}
.nodedc-modal-chip:hover {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.016) 100%),
rgba(255, 255, 255, 0.04) !important;
}
.nodedc-settings-card {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.026) 0%, rgba(255, 255, 255, 0.01) 100%),
rgba(255, 255, 255, 0.032);
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.35rem !important;
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
}
.nodedc-settings-field {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
rgba(255, 255, 255, 0.03) !important;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
transition: background 160ms ease;
}
.nodedc-settings-field:hover,
.nodedc-settings-field:focus-within {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.016) 100%),
rgba(255, 255, 255, 0.042) !important;
}
.nodedc-settings-input {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
rgba(255, 255, 255, 0.03) !important;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
min-height: 3rem;
color: var(--text-color-primary) !important;
font-size: 0.875rem !important;
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
}
.nodedc-settings-input:focus,
.nodedc-settings-input:focus-visible {
outline: none !important;
box-shadow: none !important;
}
.nodedc-settings-select {
min-height: 3rem !important;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
rgba(255, 255, 255, 0.03) !important;
color: var(--text-color-primary) !important;
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
}
.nodedc-settings-select:hover,
.nodedc-settings-select:focus-visible {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.016) 100%),
rgba(255, 255, 255, 0.042) !important;
}
.nodedc-settings-chip {
min-height: 2.75rem;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
rgba(255, 255, 255, 0.03) !important;
color: var(--text-color-primary) !important;
padding-inline: 1rem !important;
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
}
.nodedc-settings-chip:hover {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.016) 100%),
rgba(255, 255, 255, 0.042) !important;
}
.nodedc-settings-primary-button {
min-height: 2.75rem;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
background: rgb(var(--nodedc-card-active-rgb)) !important;
color: #0b1117 !important;
padding-inline: 1.35rem !important;
}
.nodedc-settings-primary-button:hover {
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
color: #0b1117 !important;
}
.nodedc-settings-save-button {
min-height: 2.75rem;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
background: rgb(var(--nodedc-card-active-rgb)) !important;
color: #0b1117 !important;
padding-inline: 1.45rem !important;
}
.nodedc-settings-save-button:hover {
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
color: #0b1117 !important;
}
.nodedc-overlay-button {
min-height: 2.5rem;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.05rem !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.018) 100%),
rgba(8, 8, 10, 0.58) !important;
color: #f5f7fb !important;
padding-inline: 1rem !important;
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
transition:
background 120ms ease,
color 120ms ease,
transform 120ms ease;
}
.nodedc-overlay-button:hover {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.075) 0%, rgba(255, 255, 255, 0.03) 100%),
rgba(8, 8, 10, 0.72) !important;
color: #ffffff !important;
}
.nodedc-settings-secondary-button {
min-height: 2.75rem;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
background: rgba(255, 255, 255, 0.06) !important;
color: var(--text-color-primary) !important;
padding-inline: 1.25rem !important;
}
.nodedc-settings-secondary-button:hover {
background: rgba(255, 255, 255, 0.1) !important;
color: var(--text-color-primary) !important;
}
.nodedc-settings-danger-button {
min-height: 2.75rem;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 1.25rem !important;
background: rgba(255, 82, 82, 0.14) !important;
color: rgb(255, 141, 141) !important;
padding-inline: 1.25rem !important;
}
.nodedc-settings-danger-button:hover {
background: rgba(255, 82, 82, 0.2) !important;
color: rgb(255, 162, 162) !important;
} }
.nodedc-toolbar-filter-toggle { .nodedc-toolbar-filter-toggle {

View File

@ -916,8 +916,11 @@ export default {
priority: "{entity} Priority", priority: "{entity} Priority",
all: "All {entity}", all: "All {entity}",
drop_here_to_move: "Drop here to move the {entity}", drop_here_to_move: "Drop here to move the {entity}",
drop_here_to_delete: "Drop here to delete the {entity}",
delete: { delete: {
label: "Delete {entity}", label: "Delete {entity}",
confirmation:
"Are you sure you want to delete {entity} {identifier}? All related data will be permanently removed and this action cannot be undone.",
success: "{entity} deleted successfully", success: "{entity} deleted successfully",
failed: "{entity} delete failed", failed: "{entity} delete failed",
}, },
@ -1865,7 +1868,9 @@ export default {
members: { members: {
label: "Members", label: "Members",
project_lead: "Project lead", project_lead: "Project lead",
project_lead_description: "Choose the project lead.",
default_assignee: "Default assignee", default_assignee: "Default assignee",
default_assignee_description: "Choose the default assignee for the project.",
guest_super_permissions: { guest_super_permissions: {
title: "Grant view access to all work items for guest users:", title: "Grant view access to all work items for guest users:",
sub_heading: "This will allow guests to have view access to all the project work items.", sub_heading: "This will allow guests to have view access to all the project work items.",
@ -1908,6 +1913,9 @@ export default {
label: "Estimates", label: "Estimates",
title: "Enable estimates for my project", title: "Enable estimates for my project",
enable_description: "They help you in communicating complexity and workload of the team.", enable_description: "They help you in communicating complexity and workload of the team.",
list_heading: "Estimate list",
archived_heading: "Archived estimates",
archived_description: "These are estimates from earlier project versions that are not currently in use. Read more",
no_estimate: "No estimate", no_estimate: "No estimate",
new: "New estimate system", new: "New estimate system",
create: { create: {

View File

@ -1072,8 +1072,11 @@ export default {
priority: "Приоритет {entity}", priority: "Приоритет {entity}",
all: "Все {entity}", all: "Все {entity}",
drop_here_to_move: "Переместите {entity} сюда", drop_here_to_move: "Переместите {entity} сюда",
drop_here_to_delete: "Перетащите сюда, чтобы удалить {entity}",
delete: { delete: {
label: "Удалить {entity}", label: "Удалить {entity}",
confirmation:
"Вы уверены, что хотите удалить {entity} {identifier}? Все связанные данные будут удалены без возможности восстановления.",
success: "{entity} успешно удалён", success: "{entity} успешно удалён",
failed: "Ошибка удаления {entity}", failed: "Ошибка удаления {entity}",
}, },
@ -2024,7 +2027,9 @@ export default {
members: { members: {
label: "Участники", label: "Участники",
project_lead: "Руководитель проекта", project_lead: "Руководитель проекта",
project_lead_description: "Выберите руководителя проекта.",
default_assignee: "Ответственный по умолчанию", default_assignee: "Ответственный по умолчанию",
default_assignee_description: "Выберите ответственного по умолчанию для проекта.",
guest_super_permissions: { guest_super_permissions: {
title: "Дать гостям доступ на просмотр всех рабочих элементов:", title: "Дать гостям доступ на просмотр всех рабочих элементов:",
sub_heading: "Гости смогут просматривать все рабочие элементы проекта", sub_heading: "Гости смогут просматривать все рабочие элементы проекта",
@ -2067,6 +2072,9 @@ export default {
label: "Оценки", label: "Оценки",
title: "Включить оценки для моего проекта", title: "Включить оценки для моего проекта",
enable_description: "Они помогают вам в общении о сложности и рабочей нагрузке команды.", enable_description: "Они помогают вам в общении о сложности и рабочей нагрузке команды.",
list_heading: "Список оценок",
archived_heading: "Архивные оценки",
archived_description: "Это оценки из предыдущих версий проекта, которые сейчас не используются. Подробнее о них",
no_estimate: "Без оценки", no_estimate: "Без оценки",
new: "Новая система оценок", new: "Новая система оценок",
create: { create: {

View File

@ -202,10 +202,10 @@ function CustomMenu(props: ICustomMenuDropdownProps) {
className={cn( className={cn(
"nodedc-glass-modal nodedc-glass-popup-surface my-1 min-w-[13rem] overflow-y-auto rounded-[1.25rem] border-0 px-2 py-2.5 text-12 whitespace-nowrap shadow-none outline-none", "nodedc-glass-modal nodedc-glass-popup-surface my-1 min-w-[13rem] overflow-y-auto rounded-[1.25rem] border-0 px-2 py-2.5 text-12 whitespace-nowrap shadow-none outline-none",
{ {
"max-h-60": maxHeight === "lg", "max-h-[min(85vh,40rem)]": maxHeight === "lg",
"max-h-48": maxHeight === "md", "max-h-[min(70vh,24rem)]": maxHeight === "md",
"max-h-36": maxHeight === "rg", "max-h-[min(55vh,18rem)]": maxHeight === "rg",
"max-h-28": maxHeight === "sm", "max-h-[min(40vh,12rem)]": maxHeight === "sm",
}, },
optionsClassName optionsClassName
)} )}

View File

@ -122,7 +122,7 @@ function CustomSelect(props: ICustomSelectProps) {
<Combobox.Options data-prevent-outside-click> <Combobox.Options data-prevent-outside-click>
<div <div
className={cn( className={cn(
"z-30 my-1 min-w-48 overflow-y-scroll rounded-md border-[0.5px] border-subtle-1 bg-surface-1 px-2 py-2.5 text-11 whitespace-nowrap focus:outline-none", "nodedc-dropdown-surface z-30 my-1 min-w-48 overflow-y-scroll whitespace-nowrap focus:outline-none",
optionsClassName optionsClassName
)} )}
ref={setPopperElement} ref={setPopperElement}
@ -166,9 +166,9 @@ function Option(props: ICustomSelectItemProps) {
value={value} value={value}
className={({ active }) => className={({ active }) =>
cn( cn(
"flex cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 text-secondary select-none", "nodedc-dropdown-option cursor-pointer text-secondary",
{ {
"bg-layer-transparent-hover": active, "bg-white/6": active,
}, },
className className
) )

View File

@ -75,8 +75,14 @@ export function AlertModalCore(props: Props) {
const Icon = VARIANT_ICONS[variant]; const Icon = VARIANT_ICONS[variant];
return ( return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={position} width={width}> <ModalCore
<div className="flex flex-col items-center gap-4 p-5 sm:flex-row sm:items-start"> isOpen={isOpen}
handleClose={handleClose}
position={position}
width={width}
className="transition-[width] ease-linear"
>
<div className="flex flex-col items-center gap-4 px-6 pt-6 pb-5 sm:flex-row sm:items-start">
{!hideIcon && ( {!hideIcon && (
<span <span
className={cn( className={cn(
@ -88,15 +94,24 @@ export function AlertModalCore(props: Props) {
</span> </span>
)} )}
<div className="text-center sm:text-left"> <div className="text-center sm:text-left">
<h3 className="text-16 font-medium">{title}</h3> <h3 className="text-18 font-medium text-secondary">{title}</h3>
<p className="mt-1 text-13 text-secondary">{content}</p> <p className="mt-1 text-13 text-secondary">{content}</p>
</div> </div>
</div> </div>
<div className="flex flex-col-reverse gap-2 border-t-[0.5px] border-subtle px-5 py-4 sm:flex-row sm:justify-end"> <div className="flex flex-col-reverse gap-3 border-t border-subtle/70 px-6 py-4 sm:flex-row sm:justify-end">
<Button variant="secondary" onClick={handleClose}> <Button variant="secondary" onClick={handleClose} className="nodedc-modal-secondary-button min-w-[8.25rem]">
{secondaryButtonText} {secondaryButtonText}
</Button> </Button>
<Button variant={BUTTON_VARIANTS[variant]} tabIndex={1} onClick={handleSubmit} loading={isSubmitting}> <Button
variant={BUTTON_VARIANTS[variant]}
tabIndex={1}
onClick={handleSubmit}
loading={isSubmitting}
className={cn("min-w-[8.25rem]", {
"nodedc-modal-danger-button": variant === "danger",
"nodedc-modal-primary-button": variant === "primary",
})}
>
{isSubmitting ? primaryButtonText.loading : primaryButtonText.default} {isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
</Button> </Button>
</div> </div>