diff --git a/HDESIGN-CODE.md b/HDESIGN-CODE.md new file mode 100644 index 0000000..6961496 --- /dev/null +++ b/HDESIGN-CODE.md @@ -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-класс/компонент и уже через него приводятся все похожие места. +- Цель: не точечная покраска одного окна, а единый системный канон. diff --git a/design.config.json b/design.config.json deleted file mode 100644 index 012a87a..0000000 --- a/design.config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "nodedc": { - "accent_rgb": [195, 255, 102], - "passive_card_rgb": [42, 43, 46], - "active_card_rgb": [195, 255, 102] - } -} diff --git a/design.config.json b/design.config.json new file mode 120000 index 0000000..c65fb28 --- /dev/null +++ b/design.config.json @@ -0,0 +1 @@ +plane-src/apps/web/design.config.json \ No newline at end of file diff --git a/plane-src/apps/web/app/root.tsx b/plane-src/apps/web/app/root.tsx index e390f35..236064a 100644 --- a/plane-src/apps/web/app/root.tsx +++ b/plane-src/apps/web/app/root.tsx @@ -4,7 +4,7 @@ * See the LICENSE file for details. */ -import type { ReactNode } from "react"; +import type { CSSProperties, ReactNode } from "react"; import Script from "next/script"; import { Links, Meta, Outlet, Scripts } 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 globalStyles from "@/styles/globals.css?url"; import type { Route } from "./+types/root"; +import designConfig from "../design.config.json"; // components import { LogoSpinner } from "@/components/common/logo-spinner"; // local @@ -35,6 +36,12 @@ import "@fontsource/ibm-plex-mono"; 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 = () => [ { rel: "icon", type: "image/png", sizes: "32x32", href: favicon32 }, { 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"); return ( - + diff --git a/plane-src/apps/web/ce/components/estimates/estimate-list-item-buttons.tsx b/plane-src/apps/web/ce/components/estimates/estimate-list-item-buttons.tsx index a289ca0..44c3a5e 100644 --- a/plane-src/apps/web/ce/components/estimates/estimate-list-item-buttons.tsx +++ b/plane-src/apps/web/ce/components/estimates/estimate-list-item-buttons.tsx @@ -24,7 +24,7 @@ export const EstimateListItemButtons = observer(function EstimateListItemButtons return (
+ } align="start" rootClassName="py-20" /> @@ -105,11 +109,10 @@ export const EstimateRoot = observer(function EstimateRoot(props: TEstimateRoot) {archivedEstimateIds && archivedEstimateIds.length > 0 && (
- Estimates have gone through a change, these are the estimates you had in your older versions which - were not in use. Read more about them  + {t("project_settings.estimates.archived_description")}  - Are you sure you want to delete work item{" "} - - {projectDetails?.identifier}-{data?.sequence_id} - - {""}? The work item will only be deleted from the intake and this action cannot be undone. - + t("inbox_issue.modals.delete.content", { + value: `${projectDetails?.identifier}-${data?.sequence_id}`, + }) } + primaryButtonText={{ + loading: t("deleting"), + default: t("delete"), + }} + secondaryButtonText={t("cancel")} /> ); }); diff --git a/plane-src/apps/web/core/components/issues/delete-issue-modal.tsx b/plane-src/apps/web/core/components/issues/delete-issue-modal.tsx index bf789d4..5f59401 100644 --- a/plane-src/apps/web/core/components/issues/delete-issue-modal.tsx +++ b/plane-src/apps/web/core/components/issues/delete-issue-modal.tsx @@ -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") })} content={ <> - {/* TODO: Translate here */} - {`Are you sure you want to delete ${isEpic ? "epic" : "work item"} `} - - {projectDetails?.identifier}-{issue?.sequence_id} - - {` ? All of the data related to the ${isEpic ? "epic" : "work item"} will be permanently removed. This action cannot be undone.`} + {t("entity.delete.confirmation", { + entity: isEpic ? t("common.epic") : t("common.work_item"), + identifier: `${projectDetails?.identifier}-${issue?.sequence_id}`, + })} } + primaryButtonText={{ + loading: t("deleting"), + default: t("delete"), + }} + secondaryButtonText={t("cancel")} /> ); }); diff --git a/plane-src/apps/web/core/components/issues/filters.tsx b/plane-src/apps/web/core/components/issues/filters.tsx index 3528433..5ab5790 100644 --- a/plane-src/apps/web/core/components/issues/filters.tsx +++ b/plane-src/apps/web/core/components/issues/filters.tsx @@ -128,7 +128,7 @@ export const HeaderFilters = observer(function HeaderFilters(props: Props) { {canUserCreateIssue ? ( @@ -566,6 +566,7 @@ export const IssueFormRoot = observer(function IssueFormRoot(props: IssueFormPro onClick={handleMoveToProjects} disabled={isMoving} size="lg" + className="nodedc-modal-primary-button min-w-[8.25rem]" > {t("add_to_project")} @@ -579,7 +580,7 @@ export const IssueFormRoot = observer(function IssueFormRoot(props: IssueFormPro {shouldRenderDuplicateModal && (
@@ -229,21 +229,22 @@ export const CreateUpdateLabelInline = observer( ref={ref} hasError={Boolean(errors.name)} placeholder={t("project_settings.labels.label_title")} - className="w-full" + className="nodedc-settings-input w-full" /> )} />
- diff --git a/plane-src/apps/web/core/components/labels/label-drag-n-drop-HOC.tsx b/plane-src/apps/web/core/components/labels/label-drag-n-drop-HOC.tsx index 483e2cf..cb019eb 100644 --- a/plane-src/apps/web/core/components/labels/label-drag-n-drop-HOC.tsx +++ b/plane-src/apps/web/core/components/labels/label-drag-n-drop-HOC.tsx @@ -33,7 +33,7 @@ export function LabelDragPreview(props: LabelDragPreviewProps) { const { label, isGroup } = props; return ( -
+
); diff --git a/plane-src/apps/web/core/components/labels/project-setting-label-group.tsx b/plane-src/apps/web/core/components/labels/project-setting-label-group.tsx index ed3e7e9..0460e45 100644 --- a/plane-src/apps/web/core/components/labels/project-setting-label-group.tsx +++ b/plane-src/apps/web/core/components/labels/project-setting-label-group.tsx @@ -78,13 +78,11 @@ export const ProjectSettingLabelGroup = observer(function ProjectSettingLabelGro {(isDragging, isDroppingInLabel, dragHandleRef) => (
{({ open }) => ( diff --git a/plane-src/apps/web/core/components/labels/project-setting-label-item.tsx b/plane-src/apps/web/core/components/labels/project-setting-label-item.tsx index 8955617..72b9307 100644 --- a/plane-src/apps/web/core/components/labels/project-setting-label-item.tsx +++ b/plane-src/apps/web/core/components/labels/project-setting-label-item.tsx @@ -87,12 +87,12 @@ export function ProjectSettingLabelItem(props: Props) { {(isDragging, isDroppingInLabel, dragHandleRef) => (
{isEditLabelForm ? ( + ) @@ -93,7 +93,7 @@ export const ProjectSettingsLabelList = observer(function ProjectSettingsLabelLi />
{showLabelForm && ( -
+
{ - newLabel(); - }, - }, - ]} + customButton={ + + } align="start" rootClassName="py-20" /> diff --git a/plane-src/apps/web/core/components/project-states/create-update/form.tsx b/plane-src/apps/web/core/components/project-states/create-update/form.tsx index 28acfdf..099a3ae 100644 --- a/plane-src/apps/web/core/components/project-states/create-update/form.tsx +++ b/plane-src/apps/web/core/components/project-states/create-update/form.tsx @@ -22,7 +22,7 @@ type TStateForm = { function PopoverButton({ color }: { color?: string }) { return (
+
{/* color */}
} panelClassName="mt-4 -ml-3"> @@ -83,7 +83,7 @@ export function StateForm(props: TStateForm) { value={formData?.name} onChange={(e) => handleFormData("name", e.target.value)} hasError={(errors && Boolean(errors.name)) || false} - className="w-full" + className="nodedc-settings-input w-full" maxLength={100} autoFocus /> @@ -96,14 +96,27 @@ export function StateForm(props: TStateForm) { value={formData?.description} onChange={(e) => handleFormData("description", e.target.value)} 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" />
- -
diff --git a/plane-src/apps/web/core/components/project-states/group-item.tsx b/plane-src/apps/web/core/components/project-states/group-item.tsx index baadd9d..fda5c56 100644 --- a/plane-src/apps/web/core/components/project-states/group-item.tsx +++ b/plane-src/apps/web/core/components/project-states/group-item.tsx @@ -57,7 +57,7 @@ export const GroupItem = observer(function GroupItem(props: TGroupItem) { return (
@@ -68,6 +68,7 @@ export const GroupItem = observer(function GroupItem(props: TGroupItem) {
-
+
{translatedGroupKey}
@@ -86,6 +87,7 @@ export const GroupItem = observer(function GroupItem(props: TGroupItem) { data-ph-element={STATE_TRACKER_ELEMENTS.STATE_GROUP_ADD_BUTTON} 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", + "rounded-full hover:bg-white/6", (!isEditable || createState) && "cursor-not-allowed text-placeholder hover:text-placeholder" )} onClick={() => { diff --git a/plane-src/apps/web/core/components/project-states/state-item.tsx b/plane-src/apps/web/core/components/project-states/state-item.tsx index 9caeef1..d6c85e9 100644 --- a/plane-src/apps/web/core/components/project-states/state-item.tsx +++ b/plane-src/apps/web/core/components/project-states/state-item.tsx @@ -135,7 +135,7 @@ export const StateItem = observer(function StateItem(props: TStateItem) {
+ {currentStep === EProjectCreationSteps.CREATE_PROJECT && ( )} @@ -113,7 +113,7 @@ function ProjectCommonAttributes(props: Props) { onChange={handleIdentifierChange(onChange)} hasError={Boolean(errors.identifier)} 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, })} tabIndex={getIndex("identifier")} @@ -144,7 +144,7 @@ function ProjectCommonAttributes(props: Props) { onChange(e); handleFormOnChange?.(); }} - className="focus:border-blue-400 !h-24 text-13" + className="nodedc-modal-input !h-24 text-13" hasError={Boolean(errors?.description)} tabIndex={getIndex("description")} /> diff --git a/plane-src/apps/web/core/components/project/create/header.tsx b/plane-src/apps/web/core/components/project/create/header.tsx index 98ab84d..7e7d84c 100644 --- a/plane-src/apps/web/core/components/project/create/header.tsx +++ b/plane-src/apps/web/core/components/project/create/header.tsx @@ -48,11 +48,11 @@ function ProjectCreateHeader(props: Props) { const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile); return ( -
+
{showActionButtons && (
@@ -60,8 +60,13 @@ function ProjectCreateHeader(props: Props) {
)} {isClosable && ( -
-
@@ -79,6 +84,7 @@ function ProjectCreateHeader(props: Props) { }} control={control} value={value ?? null} + buttonClassName="nodedc-overlay-button" tabIndex={getIndex("cover_image")} /> )} @@ -96,7 +102,7 @@ function ProjectCreateHeader(props: Props) { className="flex items-center justify-center" buttonClassName="flex items-center justify-center" label={ - + } diff --git a/plane-src/apps/web/core/components/project/create/project-create-buttons.tsx b/plane-src/apps/web/core/components/project/create/project-create-buttons.tsx index ae66ca7..aa10302 100644 --- a/plane-src/apps/web/core/components/project/create/project-create-buttons.tsx +++ b/plane-src/apps/web/core/components/project/create/project-create-buttons.tsx @@ -30,10 +30,23 @@ function ProjectCreateButtons(props: Props) { return (
- -
diff --git a/plane-src/apps/web/core/components/project/dropdowns/filters/member-list.tsx b/plane-src/apps/web/core/components/project/dropdowns/filters/member-list.tsx index bb8acd1..8996b49 100644 --- a/plane-src/apps/web/core/components/project/dropdowns/filters/member-list.tsx +++ b/plane-src/apps/web/core/components/project/dropdowns/filters/member-list.tsx @@ -7,6 +7,7 @@ import { useState } from "react"; import { observer } from "mobx-react"; // plane imports +import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; import { ChevronDownIcon } from "@plane/propel/icons"; import { EUserProjectRoles, EUserWorkspaceRoles } from "@plane/types"; @@ -52,11 +53,12 @@ const RoleFilterGroup = observer(function RoleFilterGroup({ const [isExpanded, setIsExpanded] = useState(true); const appliedFiltersCount = appliedFilters?.length ?? 0; const roleOptions = memberType === "project" ? PROJECT_ROLE_OPTIONS : WORKSPACE_ROLE_OPTIONS; + const { t } = useTranslation(); return (
0 ? ` (${appliedFiltersCount})` : ""}`} + title={`${t("roles")}${appliedFiltersCount > 0 ? ` (${appliedFiltersCount})` : ""}`} isPreviewEnabled={isExpanded} handleIsPreviewEnabled={() => setIsExpanded(!isExpanded)} /> @@ -96,13 +98,14 @@ export const MemberListFiltersDropdown = observer(function MemberListFiltersDrop const { appliedFilters, handleUpdate, memberType } = props; const appliedFiltersCount = appliedFilters?.length ?? 0; + const { t } = useTranslation(); return ( - {appliedFiltersCount > 0 && ( diff --git a/plane-src/apps/web/core/components/project/form.tsx b/plane-src/apps/web/core/components/project/form.tsx index 635bbc9..1d0cd66 100644 --- a/plane-src/apps/web/core/components/project/form.tsx +++ b/plane-src/apps/web/core/components/project/form.tsx @@ -250,16 +250,17 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) { control={control} name="cover_image_url" render={({ field: { value, onChange } }) => ( - - )} - /> + + )} + />
@@ -286,7 +287,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) { value={value} onChange={onChange} 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")} disabled={!isAdmin} /> @@ -306,7 +307,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) { value={value} placeholder={t("project_description_placeholder")} 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)} disabled={!isAdmin} /> @@ -342,7 +343,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) { ref={ref} hasError={Boolean(errors.identifier)} 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} /> )} @@ -383,7 +384,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) { )}
} - buttonClassName="!border-subtle !shadow-none font-medium rounded-md" + buttonClassName="nodedc-settings-select !h-12 font-medium" input disabled={!isAdmin} // optionsClassName="w-full" @@ -418,7 +419,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) { onChange(value); }} error={Boolean(errors.timezone)} - buttonClassName="border-none" + buttonClassName="nodedc-settings-select !h-12 !border-0" disabled={!isAdmin} /> @@ -429,7 +430,14 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
<> - diff --git a/plane-src/apps/web/core/components/project/member-list.tsx b/plane-src/apps/web/core/components/project/member-list.tsx index 3c72ef5..f275c83 100644 --- a/plane-src/apps/web/core/components/project/member-list.tsx +++ b/plane-src/apps/web/core/components/project/member-list.tsx @@ -84,11 +84,11 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro
{t("common.members")}
-
+
setSearchQuery(e.target.value)} @@ -101,11 +101,12 @@ export const ProjectMemberList = observer(function ProjectMemberList(props: TPro /> {isAdmin && ( diff --git a/plane-src/apps/web/core/components/project/member-select.tsx b/plane-src/apps/web/core/components/project/member-select.tsx index b55609c..2a9b67f 100644 --- a/plane-src/apps/web/core/components/project/member-select.tsx +++ b/plane-src/apps/web/core/components/project/member-select.tsx @@ -8,6 +8,7 @@ import React from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { Ban } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; import { EUserProjectRoles } from "@plane/types"; // plane ui import { Avatar, CustomSearchSelect } from "@plane/ui"; @@ -24,6 +25,7 @@ type Props = { export const MemberSelect = observer(function MemberSelect(props: Props) { const { value, onChange, isDisabled = false } = props; + const { t } = useTranslation(); // router const { projectId } = useParams(); // store hooks @@ -72,12 +74,12 @@ export const MemberSelect = observer(function MemberSelect(props: Props) { ) : (
- None + {t("none")}
)}
} - buttonClassName="!px-3 !py-2 bg-surface-1" + buttonClassName="nodedc-settings-select !w-full !justify-between !px-4 !py-3" options={ options && options && [ @@ -88,7 +90,7 @@ export const MemberSelect = observer(function MemberSelect(props: Props) { content: (
- None + {t("none")}
), }, diff --git a/plane-src/apps/web/core/components/project/project-feature-update.tsx b/plane-src/apps/web/core/components/project/project-feature-update.tsx index b2eac27..95f8df4 100644 --- a/plane-src/apps/web/core/components/project/project-feature-update.tsx +++ b/plane-src/apps/web/core/components/project/project-feature-update.tsx @@ -8,7 +8,7 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useTranslation } from "@plane/i18n"; // ui -import { Button, getButtonStyling } from "@plane/propel/button"; +import { Button } from "@plane/propel/button"; import { Logo } from "@plane/propel/emoji-icon-picker"; import { Row } from "@plane/ui"; // components @@ -35,7 +35,7 @@ export const ProjectFeatureUpdate = observer(function ProjectFeatureUpdate(props return ( <> - +
@@ -45,13 +45,19 @@ export const ProjectFeatureUpdate = observer(function ProjectFeatureUpdate(props {t("created").toLowerCase()}.
- {t("open_project")} diff --git a/plane-src/apps/web/core/components/project/project-settings-member-defaults.tsx b/plane-src/apps/web/core/components/project/project-settings-member-defaults.tsx index e505b7b..2813383 100644 --- a/plane-src/apps/web/core/components/project/project-settings-member-defaults.tsx +++ b/plane-src/apps/web/core/components/project/project-settings-member-defaults.tsx @@ -36,9 +36,9 @@ type TDefaultSettingItemProps = { function DefaultSettingItem({ title, description, children }: TDefaultSettingItemProps) { return ( -
-
-

{title}

+
+
+

{title}

{description}

{children}
@@ -139,7 +139,10 @@ export const ProjectSettingsMemberDefaults = observer(function ProjectSettingsMe return (
- + {currentProjectDetails ? ( )} - + {currentProjectDetails ? ( {currentProjectDetails && (
setSelectedProject(null)} /> -
- {/* Project Selector */} +
setArchiveProject(true)}> + } /> - {/* Format Selector */} setSelectedProject(currentProjectDetails.id ?? null)} data-ph-element={PROJECT_TRACKER_ELEMENTS.DELETE_PROJECT_BUTTON} + className="nodedc-settings-danger-button" > {t("delete")} diff --git a/plane-src/apps/web/core/components/settings/boxed-control-item.tsx b/plane-src/apps/web/core/components/settings/boxed-control-item.tsx index 2428d58..c4fda18 100644 --- a/plane-src/apps/web/core/components/settings/boxed-control-item.tsx +++ b/plane-src/apps/web/core/components/settings/boxed-control-item.tsx @@ -20,7 +20,7 @@ export function SettingsBoxedControlItem(props: Props) { return (
diff --git a/plane-src/apps/web/design.config.json b/plane-src/apps/web/design.config.json new file mode 100644 index 0000000..012a87a --- /dev/null +++ b/plane-src/apps/web/design.config.json @@ -0,0 +1,7 @@ +{ + "nodedc": { + "accent_rgb": [195, 255, 102], + "passive_card_rgb": [42, 43, 46], + "active_card_rgb": [195, 255, 102] + } +} diff --git a/plane-src/apps/web/public/sw.js b/plane-src/apps/web/public/sw.js index afb1d51..85d7fbd 100644 --- a/plane-src/apps/web/public/sw.js +++ b/plane-src/apps/web/public/sw.js @@ -1,102 +1,33 @@ -/** - * Copyright 2018 Google Inc. All Rights Reserved. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * 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. +/* + * Temporary cleanup service worker. + * + * We intentionally stop intercepting requests, clear legacy caches, and + * unregister ourselves. This prevents stale HTML/chunk bundles from surviving + * fresh web deployments and causing client-side hydration/runtime crashes. */ -// If the loader is already loaded, just stop. -if (!self.define) { - let registry = {}; +self.addEventListener("install", (event) => { + event.waitUntil(self.skipWaiting()); +}); - // Used for `eval` and `importScripts` where we can't get script URL by other means. - // In both cases, it's safe to use a global var because those functions are synchronous. - let nextDefineUri; +self.addEventListener("activate", (event) => { + event.waitUntil( + (async () => { + const cacheKeys = await caches.keys(); + await Promise.all(cacheKeys.map((cacheKey) => caches.delete(cacheKey))); - const singleRequire = (uri, parentUri) => { - uri = new URL(uri + ".js", parentUri).href; - return ( - registry[uri] || - 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} didn’t register its module`); - } - return promise; - }) - ); - }; + const clients = await self.clients.matchAll({ type: "window", includeUncontrolled: true }); + await Promise.all( + clients.map((client) => + "navigate" in client && typeof client.navigate === "function" ? client.navigate(client.url) : Promise.resolve() + ) + ); - self.define = (depsNames, factory) => { - 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" + await self.registration.unregister(); + })() ); }); -//# sourceMappingURL=sw.js.map + +self.addEventListener("fetch", () => { + // Intentionally empty: do not cache or intercept runtime requests. +}); diff --git a/plane-src/apps/web/styles/globals.css b/plane-src/apps/web/styles/globals.css index 151e916..a39b52f 100644 --- a/plane-src/apps/web/styles/globals.css +++ b/plane-src/apps/web/styles/globals.css @@ -30,6 +30,9 @@ --nodedc-accent-rgb: 51 163 255; --nodedc-card-passive-rgb: 42 43 46; --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 */ } /* background colors */ @@ -419,7 +422,7 @@ box-shadow: none !important; border-radius: 999px !important; min-height: 2.5rem; - padding-inline: 1rem; + padding-inline: 1.35rem; background: rgba(18, 18, 22, 0.94) !important; color: var(--text-color-primary) !important; transition: @@ -428,6 +431,11 @@ transform 160ms ease; } + .nodedc-toolbar-pill-wide { + min-width: 9.75rem; + padding-inline: 1.75rem; + } + .nodedc-toolbar-pill:hover { background: rgba(24, 24, 29, 0.96) !important; } @@ -442,13 +450,261 @@ box-shadow: none !important; border-radius: 999px !important; min-height: 2.5rem; - padding-inline: 1rem; + padding-inline: 1.55rem; background: rgb(var(--nodedc-accent-rgb)) !important; color: #0b1117 !important; } + .nodedc-toolbar-primary-wide { + min-width: 13rem; + padding-inline: 2rem; + } + .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 { diff --git a/plane-src/packages/i18n/src/locales/en/translations.ts b/plane-src/packages/i18n/src/locales/en/translations.ts index 08485c9..974a18e 100644 --- a/plane-src/packages/i18n/src/locales/en/translations.ts +++ b/plane-src/packages/i18n/src/locales/en/translations.ts @@ -916,8 +916,11 @@ export default { priority: "{entity} Priority", all: "All {entity}", drop_here_to_move: "Drop here to move the {entity}", + drop_here_to_delete: "Drop here to delete the {entity}", delete: { 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", failed: "{entity} delete failed", }, @@ -1865,7 +1868,9 @@ export default { members: { label: "Members", project_lead: "Project lead", + project_lead_description: "Choose the project lead.", default_assignee: "Default assignee", + default_assignee_description: "Choose the default assignee for the project.", guest_super_permissions: { 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.", @@ -1908,6 +1913,9 @@ export default { label: "Estimates", title: "Enable estimates for my project", 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", new: "New estimate system", create: { diff --git a/plane-src/packages/i18n/src/locales/ru/translations.ts b/plane-src/packages/i18n/src/locales/ru/translations.ts index 0b4934e..55ea8f6 100644 --- a/plane-src/packages/i18n/src/locales/ru/translations.ts +++ b/plane-src/packages/i18n/src/locales/ru/translations.ts @@ -1072,8 +1072,11 @@ export default { priority: "Приоритет {entity}", all: "Все {entity}", drop_here_to_move: "Переместите {entity} сюда", + drop_here_to_delete: "Перетащите сюда, чтобы удалить {entity}", delete: { label: "Удалить {entity}", + confirmation: + "Вы уверены, что хотите удалить {entity} {identifier}? Все связанные данные будут удалены без возможности восстановления.", success: "{entity} успешно удалён", failed: "Ошибка удаления {entity}", }, @@ -2024,7 +2027,9 @@ export default { members: { label: "Участники", project_lead: "Руководитель проекта", + project_lead_description: "Выберите руководителя проекта.", default_assignee: "Ответственный по умолчанию", + default_assignee_description: "Выберите ответственного по умолчанию для проекта.", guest_super_permissions: { title: "Дать гостям доступ на просмотр всех рабочих элементов:", sub_heading: "Гости смогут просматривать все рабочие элементы проекта", @@ -2067,6 +2072,9 @@ export default { label: "Оценки", title: "Включить оценки для моего проекта", enable_description: "Они помогают вам в общении о сложности и рабочей нагрузке команды.", + list_heading: "Список оценок", + archived_heading: "Архивные оценки", + archived_description: "Это оценки из предыдущих версий проекта, которые сейчас не используются. Подробнее о них", no_estimate: "Без оценки", new: "Новая система оценок", create: { diff --git a/plane-src/packages/ui/src/dropdowns/custom-menu.tsx b/plane-src/packages/ui/src/dropdowns/custom-menu.tsx index 8a58e9e..a0f5665 100644 --- a/plane-src/packages/ui/src/dropdowns/custom-menu.tsx +++ b/plane-src/packages/ui/src/dropdowns/custom-menu.tsx @@ -202,10 +202,10 @@ function CustomMenu(props: ICustomMenuDropdownProps) { 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", { - "max-h-60": maxHeight === "lg", - "max-h-48": maxHeight === "md", - "max-h-36": maxHeight === "rg", - "max-h-28": maxHeight === "sm", + "max-h-[min(85vh,40rem)]": maxHeight === "lg", + "max-h-[min(70vh,24rem)]": maxHeight === "md", + "max-h-[min(55vh,18rem)]": maxHeight === "rg", + "max-h-[min(40vh,12rem)]": maxHeight === "sm", }, optionsClassName )} diff --git a/plane-src/packages/ui/src/dropdowns/custom-select.tsx b/plane-src/packages/ui/src/dropdowns/custom-select.tsx index cfdb986..7b5c6f7 100644 --- a/plane-src/packages/ui/src/dropdowns/custom-select.tsx +++ b/plane-src/packages/ui/src/dropdowns/custom-select.tsx @@ -122,7 +122,7 @@ function CustomSelect(props: ICustomSelectProps) {
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 ) diff --git a/plane-src/packages/ui/src/modals/alert-modal.tsx b/plane-src/packages/ui/src/modals/alert-modal.tsx index 5565ab0..cfbd8e0 100644 --- a/plane-src/packages/ui/src/modals/alert-modal.tsx +++ b/plane-src/packages/ui/src/modals/alert-modal.tsx @@ -75,8 +75,14 @@ export function AlertModalCore(props: Props) { const Icon = VARIANT_ICONS[variant]; return ( - -
+ +
{!hideIcon && ( )}
-

{title}

+

{title}

{content}

-
- -