UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: уплотнение workspace home и интерактивный Гант
This commit is contained in:
parent
4c436a949e
commit
e5036fc95b
|
|
@ -4,37 +4,105 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SlidersHorizontal } from "lucide-react";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { Button } from "@plane/propel/button";
|
import type { IUser, TActivityEntityData, TProjectAnalyticsCount } from "@plane/types";
|
||||||
import { useHome } from "@/hooks/store/use-home";
|
import { useCurrentTime } from "@/hooks/use-current-time";
|
||||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
import { getActivityProjectId, getCompletionRate, type THomeProjectData } from "@/components/home/home.utils";
|
||||||
|
|
||||||
export function HomePageHeader() {
|
type HomePageHeaderProps = {
|
||||||
const { t } = useTranslation();
|
currentUser?: IUser;
|
||||||
const { toggleWidgetSettings } = useHome();
|
selectedProject?: THomeProjectData;
|
||||||
const { currentWorkspace } = useWorkspace();
|
selectedProjectAnalytics?: TProjectAnalyticsCount;
|
||||||
|
recents?: TActivityEntityData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function HomePageHeader(props: HomePageHeaderProps) {
|
||||||
|
const { currentUser, selectedProject, selectedProjectAnalytics, recents } = props;
|
||||||
|
const { currentLocale } = useTranslation();
|
||||||
|
const { currentTime } = useCurrentTime();
|
||||||
|
|
||||||
|
const timeString = new Intl.DateTimeFormat(currentLocale, {
|
||||||
|
timeZone: currentUser?.user_timezone,
|
||||||
|
hour12: false,
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}).format(currentTime);
|
||||||
|
const dateString = new Intl.DateTimeFormat(currentLocale, {
|
||||||
|
weekday: "long",
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
}).format(currentTime);
|
||||||
|
const heroDateLabel = dateString.toLocaleUpperCase(currentLocale || "ru-RU");
|
||||||
|
|
||||||
|
const totalIssues = selectedProjectAnalytics?.total_issues ?? 0;
|
||||||
|
const completedIssues = selectedProjectAnalytics?.completed_issues ?? 0;
|
||||||
|
const openIssues = Math.max(totalIssues - completedIssues, 0);
|
||||||
|
const completionRate = getCompletionRate(selectedProjectAnalytics);
|
||||||
|
const recentTouchpoints = (recents ?? []).filter((activity) => {
|
||||||
|
if (!selectedProject) return true;
|
||||||
|
return getActivityProjectId(activity) === selectedProject.id;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const marketMetrics = [
|
||||||
|
{ label: "Готовность", value: `${completionRate}%`, caption: `${completedIssues}/${totalIssues || 0}` },
|
||||||
|
{ label: "Открытые задачи", value: openIssues.toString(), caption: "в работе" },
|
||||||
|
{ label: "Касания 7 дней", value: recentTouchpoints.toString(), caption: "recent" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<section className="nodedc-home-hero">
|
||||||
<div className="flex min-w-0 flex-col gap-1">
|
<div className="nodedc-home-hero-time">
|
||||||
<div className="inline-flex w-fit items-center gap-2 rounded-full bg-white/6 px-3 py-1.5 text-[11px] font-semibold tracking-[0.22em] text-placeholder uppercase">
|
<div className="text-[11px] font-semibold tracking-[0.16em] text-placeholder uppercase">{heroDateLabel}</div>
|
||||||
<span>Workspace Home</span>
|
<div className="text-13 font-semibold text-primary">{timeString}</div>
|
||||||
</div>
|
|
||||||
<div className="text-13 text-secondary">
|
|
||||||
{currentWorkspace?.name ? `Стартовый экран для ${currentWorkspace.name}` : "Главная страница workspace"}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className="nodedc-home-hero-grid">
|
||||||
variant="secondary"
|
<div className="nodedc-home-hero-title-cell">
|
||||||
size="lg"
|
<h1>WORKSPACE HOME</h1>
|
||||||
className="nodedc-toolbar-pill"
|
<p>
|
||||||
prependIcon={<SlidersHorizontal className="size-4" />}
|
{selectedProject
|
||||||
onClick={() => toggleWidgetSettings(true)}
|
? `${selectedProject.identifier} в фокусе домашней сводки.`
|
||||||
>
|
: "Выберите проект для фокуса, Ганта и рабочей аналитики."}
|
||||||
{t("home.manage_widgets")}
|
</p>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div className="nodedc-home-market-band">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-12 font-semibold text-black/[0.58]">Фокус</div>
|
||||||
|
<div className="mt-1 flex min-w-0 items-center gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-24 leading-none font-semibold text-black">
|
||||||
|
{selectedProject?.name ?? "Workspace"}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 truncate text-12 font-medium text-black/[0.54]">
|
||||||
|
{selectedProject?.description || selectedProject?.identifier || "Координационный обзор"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid min-w-0 flex-1 gap-3 sm:grid-cols-3">
|
||||||
|
{marketMetrics.map((metric) => (
|
||||||
|
<div key={metric.label} className="min-w-0">
|
||||||
|
<div className="truncate text-12 font-medium text-black/[0.58]">{metric.label}</div>
|
||||||
|
<div className="mt-1 text-[26px] leading-none font-semibold text-black">{metric.value}</div>
|
||||||
|
<div className="mt-2 h-2 overflow-hidden rounded-full bg-black/[0.18]">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-black"
|
||||||
|
style={{
|
||||||
|
width:
|
||||||
|
metric.label === "Готовность"
|
||||||
|
? `${Math.min(completionRate, 100)}%`
|
||||||
|
: `${Math.min(Number(metric.value) * 8 + 18, 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-11 text-black/[0.48]">{metric.caption}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,36 +7,29 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import type { IUser, THomeWidgetKeys, THomeWidgetProps, TProjectAnalyticsCount } from "@plane/types";
|
import type { IUser, THomeWidgetKeys, THomeWidgetProps, TProjectAnalyticsCount } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// assets
|
|
||||||
import darkWidgetsAsset from "@/app/assets/empty-state/dashboard/widgets-dark.webp?url";
|
|
||||||
import lightWidgetsAsset from "@/app/assets/empty-state/dashboard/widgets-light.webp?url";
|
|
||||||
// components
|
|
||||||
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useHome } from "@/hooks/store/use-home";
|
import { useHome } from "@/hooks/store/use-home";
|
||||||
import { useProject } from "@/hooks/store/use-project";
|
import { useProject } from "@/hooks/store/use-project";
|
||||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
|
||||||
// plane web components
|
// plane web components
|
||||||
import { HomePageHeader } from "@/plane-web/components/home/header";
|
import { HomePageHeader } from "@/plane-web/components/home/header";
|
||||||
import { ProjectService } from "@/services/project";
|
import { ProjectService } from "@/services/project";
|
||||||
import { WorkspaceService } from "@/services/workspace.service";
|
import { WorkspaceService } from "@/services/workspace.service";
|
||||||
// local imports
|
// local imports
|
||||||
import { HomeCardShell } from "./home-card-shell";
|
import { HomeCardShell } from "./home-card-shell";
|
||||||
|
import { HomeGanttPreview } from "./home-gantt-preview";
|
||||||
import { HomeRecentIssueDecks } from "./home-recent-issue-decks";
|
import { HomeRecentIssueDecks } from "./home-recent-issue-decks";
|
||||||
import { HomeProjectInsights } from "./home-project-insights";
|
import { HomeActivityTrendCard, HomeOperationsOverview } from "./home-project-insights";
|
||||||
import { HomeProjectStack } from "./home-project-stack";
|
import { HomeProjectStack } from "./home-project-stack";
|
||||||
import { aggregateProjectAnalytics, type THomeProjectData } from "./home.utils";
|
import { aggregateProjectAnalytics, type THomeProjectData } from "./home.utils";
|
||||||
import { StickiesWidget } from "../stickies/widget";
|
import { StickiesWidget } from "../stickies/widget";
|
||||||
import { HomeLoader, NoProjectsEmptyState, RecentActivityWidget } from "./widgets";
|
import { HomeLoader, NoProjectsEmptyState, RecentActivityWidget } from "./widgets";
|
||||||
import { DashboardQuickLinks } from "./widgets/links";
|
import { DashboardQuickLinks } from "./widgets/links";
|
||||||
import { ManageWidgetsModal } from "./widgets/manage";
|
import { ManageWidgetsModal } from "./widgets/manage";
|
||||||
import { UserGreetingsView } from "./user-greetings";
|
|
||||||
|
|
||||||
const projectService = new ProjectService();
|
const projectService = new ProjectService();
|
||||||
const workspaceService = new WorkspaceService();
|
const workspaceService = new WorkspaceService();
|
||||||
|
|
@ -86,19 +79,14 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
|
||||||
const workspaceSlugValue = Array.isArray(workspaceSlug) ? workspaceSlug[0] : workspaceSlug?.toString();
|
const workspaceSlugValue = Array.isArray(workspaceSlug) ? workspaceSlug[0] : workspaceSlug?.toString();
|
||||||
// navigation
|
// navigation
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
// theme hook
|
|
||||||
const { resolvedTheme } = useTheme();
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { toggleWidgetSettings, widgetsMap, showWidgetSettings, loading } = useHome();
|
const { toggleWidgetSettings, widgetsMap, showWidgetSettings, loading } = useHome();
|
||||||
const { loader, joinedProjectIds, getPartialProjectById, fetchProjectAnalyticsCount, getProjectAnalyticsCountById } =
|
const { loader, joinedProjectIds, getPartialProjectById, fetchProjectAnalyticsCount, getProjectAnalyticsCountById } =
|
||||||
useProject();
|
useProject();
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
// plane hooks
|
// plane hooks
|
||||||
const { t, currentLocale } = useTranslation();
|
const { currentLocale } = useTranslation();
|
||||||
// states
|
// states
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
||||||
// derived values
|
|
||||||
const noWidgetsResolvedPath = resolvedTheme === "light" ? lightWidgetsAsset : darkWidgetsAsset;
|
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const isWikiApp = workspaceSlugValue ? pathname.includes(`/${workspaceSlugValue}/pages`) : false;
|
const isWikiApp = workspaceSlugValue ? pathname.includes(`/${workspaceSlugValue}/pages`) : false;
|
||||||
|
|
@ -174,19 +162,18 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
|
||||||
const isRecentsEnabled = !!widgetsMap.recents?.is_enabled;
|
const isRecentsEnabled = !!widgetsMap.recents?.is_enabled;
|
||||||
const isQuickLinksEnabled = !!widgetsMap.quick_links?.is_enabled;
|
const isQuickLinksEnabled = !!widgetsMap.quick_links?.is_enabled;
|
||||||
const isStickiesEnabled = !!widgetsMap.my_stickies?.is_enabled;
|
const isStickiesEnabled = !!widgetsMap.my_stickies?.is_enabled;
|
||||||
const hasDashboardContent = isRecentsEnabled || isQuickLinksEnabled || isStickiesEnabled;
|
const hasSecondaryWidgets = isQuickLinksEnabled || isStickiesEnabled;
|
||||||
|
|
||||||
if (!workspaceSlugValue) return null;
|
if (!workspaceSlugValue) return null;
|
||||||
if (loading || loader !== "loaded") return <HomeLoader />;
|
if (loading || loader !== "loaded") return <HomeLoader />;
|
||||||
|
|
||||||
const recentsCard = isRecentsEnabled ? (
|
const recentActivityCard = isRecentsEnabled ? (
|
||||||
<HomeCardShell className="overflow-hidden" contentClassName="p-5">
|
<RecentActivityWidget
|
||||||
<RecentActivityWidget
|
recents={workspaceRecents}
|
||||||
workspaceSlug={workspaceSlugValue}
|
projectId={selectedProject?.id ?? null}
|
||||||
recents={workspaceRecents}
|
showFilterSelect={false}
|
||||||
projectId={selectedProjectId}
|
workspaceSlug={workspaceSlugValue}
|
||||||
/>
|
/>
|
||||||
</HomeCardShell>
|
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const sideWidgetCards = [
|
const sideWidgetCards = [
|
||||||
|
|
@ -204,78 +191,69 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full flex-col gap-6">
|
<div className="relative flex h-full w-full flex-col gap-6">
|
||||||
<HomePageHeader />
|
<HomePageHeader
|
||||||
|
currentUser={currentUser}
|
||||||
|
selectedProject={selectedProject}
|
||||||
|
selectedProjectAnalytics={selectedProjectAnalytics}
|
||||||
|
recents={workspaceRecents}
|
||||||
|
/>
|
||||||
<ManageWidgetsModal
|
<ManageWidgetsModal
|
||||||
workspaceSlug={workspaceSlugValue}
|
workspaceSlug={workspaceSlugValue}
|
||||||
isModalOpen={showWidgetSettings}
|
isModalOpen={showWidgetSettings}
|
||||||
handleOnClose={() => toggleWidgetSettings(false)}
|
handleOnClose={() => toggleWidgetSettings(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[340px_minmax(0,1fr)]">
|
<div className="grid gap-5 xl:grid-cols-[minmax(320px,360px)_minmax(0,1fr)] xl:items-stretch">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<HomeProjectStack
|
<HomeProjectStack
|
||||||
|
className="h-full"
|
||||||
projects={homeProjects}
|
projects={homeProjects}
|
||||||
analyticsMap={analyticsMap}
|
analyticsMap={analyticsMap}
|
||||||
recents={workspaceRecents}
|
recents={workspaceRecents}
|
||||||
selectedProjectId={selectedProjectId}
|
selectedProjectId={selectedProjectId}
|
||||||
onSelectProject={setSelectedProjectId}
|
onSelectProject={setSelectedProjectId}
|
||||||
workspaceSlug={workspaceSlugValue}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 space-y-5">
|
<div className="min-w-0 space-y-5">
|
||||||
{currentUser && (
|
<HomeGanttPreview
|
||||||
<UserGreetingsView
|
project={selectedProject}
|
||||||
user={currentUser}
|
analytics={selectedProjectAnalytics}
|
||||||
workspaceName={currentWorkspace?.name}
|
workspaceSlug={workspaceSlugValue}
|
||||||
selectedProject={selectedProject}
|
/>
|
||||||
selectedProjectAnalytics={selectedProjectAnalytics}
|
<HomeOperationsOverview
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<HomeRecentIssueDecks project={selectedProject} workspaceSlug={workspaceSlugValue} />
|
|
||||||
|
|
||||||
<HomeProjectInsights
|
|
||||||
project={selectedProject}
|
project={selectedProject}
|
||||||
analytics={selectedProjectAnalytics}
|
analytics={selectedProjectAnalytics}
|
||||||
analyticsCollection={analyticsCollection}
|
analyticsCollection={analyticsCollection}
|
||||||
recents={workspaceRecents}
|
recents={workspaceRecents}
|
||||||
locale={currentLocale || "ru-RU"}
|
recentActivitySlot={recentActivityCard}
|
||||||
|
locale={currentLocale}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isWikiApp && <NoProjectsEmptyState />}
|
|
||||||
|
|
||||||
{hasDashboardContent ? (
|
|
||||||
<>
|
|
||||||
{recentsCard && sideWidgetCards.length > 0 ? (
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[minmax(0,1.08fr)_minmax(320px,0.92fr)]">
|
|
||||||
{recentsCard}
|
|
||||||
<div className="grid min-w-0 gap-5">{sideWidgetCards}</div>
|
|
||||||
</div>
|
|
||||||
) : recentsCard ? (
|
|
||||||
recentsCard
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={cn("grid gap-5", {
|
|
||||||
"md:grid-cols-2": sideWidgetCards.length > 1,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{sideWidgetCards}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<HomeCardShell className="overflow-hidden" contentClassName="p-8">
|
|
||||||
<div className="grid min-h-[260px] place-items-center">
|
|
||||||
<SimpleEmptyState
|
|
||||||
title={t("home.empty.widgets.title")}
|
|
||||||
description={t("home.empty.widgets.description")}
|
|
||||||
assetPath={noWidgetsResolvedPath}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</HomeCardShell>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<HomeActivityTrendCard
|
||||||
|
project={selectedProject}
|
||||||
|
analytics={selectedProjectAnalytics}
|
||||||
|
analyticsCollection={analyticsCollection}
|
||||||
|
recents={workspaceRecents}
|
||||||
|
locale={currentLocale}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HomeRecentIssueDecks project={selectedProject} workspaceSlug={workspaceSlugValue} />
|
||||||
|
|
||||||
|
<div className="space-y-5">
|
||||||
|
{!isWikiApp && <NoProjectsEmptyState />}
|
||||||
|
|
||||||
|
{hasSecondaryWidgets && (
|
||||||
|
<div
|
||||||
|
className={cn("grid gap-5", {
|
||||||
|
"md:grid-cols-2": sideWidgetCards.length > 1,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{sideWidgetCards}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,258 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { CalendarDays, Filter, SlidersHorizontal } from "lucide-react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import type { TIssue, TProjectAnalyticsCount } from "@plane/types";
|
||||||
|
import { cn } from "@plane/utils";
|
||||||
|
import { IssueService } from "@/services/issue";
|
||||||
|
import { getCompletionRate, type THomeProjectData } from "./home.utils";
|
||||||
|
|
||||||
|
const issueService = new IssueService();
|
||||||
|
const GANTT_PREVIEW_LIMIT = 6;
|
||||||
|
const GANTT_PREVIEW_CURSOR = `${GANTT_PREVIEW_LIMIT}:0:0`;
|
||||||
|
|
||||||
|
type HomeGanttPreviewProps = {
|
||||||
|
analytics?: TProjectAnalyticsCount;
|
||||||
|
project?: THomeProjectData;
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TGanttPreviewItem = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
subtitle: string;
|
||||||
|
start: number;
|
||||||
|
width: number;
|
||||||
|
tone: "accent" | "muted" | "white";
|
||||||
|
};
|
||||||
|
|
||||||
|
const GANTT_RANGES = ["Live", "1D", "1W", "1M"] as const;
|
||||||
|
type TGanttRange = (typeof GANTT_RANGES)[number];
|
||||||
|
|
||||||
|
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
|
||||||
|
|
||||||
|
const buildSyntheticItems = (project: THomeProjectData | undefined, analytics: TProjectAnalyticsCount | undefined) => {
|
||||||
|
const completionRate = getCompletionRate(analytics);
|
||||||
|
const openIssues = Math.max((analytics?.total_issues ?? 0) - (analytics?.completed_issues ?? 0), 0);
|
||||||
|
const baseName = project?.identifier ?? "NODE";
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "synthetic-approval",
|
||||||
|
label: "Согласование расходов",
|
||||||
|
subtitle: `${baseName} / финконтроль`,
|
||||||
|
start: 6,
|
||||||
|
width: clamp(34 + completionRate * 0.22, 26, 58),
|
||||||
|
tone: "accent",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "synthetic-docs",
|
||||||
|
label: "Контроль документов",
|
||||||
|
subtitle: `${baseName} / внешний обмен`,
|
||||||
|
start: 22,
|
||||||
|
width: clamp(28 + openIssues * 2, 24, 54),
|
||||||
|
tone: "white",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "synthetic-sync",
|
||||||
|
label: "Синхронизация статусов",
|
||||||
|
subtitle: `${baseName} / внутренний контур`,
|
||||||
|
start: 42,
|
||||||
|
width: 36,
|
||||||
|
tone: "muted",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "synthetic-close",
|
||||||
|
label: "Закрытие остатка",
|
||||||
|
subtitle: `${baseName} / итог недели`,
|
||||||
|
start: 58,
|
||||||
|
width: 28,
|
||||||
|
tone: "accent",
|
||||||
|
},
|
||||||
|
] satisfies TGanttPreviewItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildIssueItems = (issues: TIssue[], project: THomeProjectData): TGanttPreviewItem[] =>
|
||||||
|
issues.slice(0, GANTT_PREVIEW_LIMIT).map((issue, index) => {
|
||||||
|
const createdDate = Date.parse(issue.created_at ?? "") || Date.now();
|
||||||
|
const targetDate = Date.parse(issue.target_date ?? "") || createdDate + (index + 3) * 24 * 60 * 60 * 1000;
|
||||||
|
const durationDays = Math.max((targetDate - createdDate) / (24 * 60 * 60 * 1000), 1);
|
||||||
|
const start = clamp((index * 13 + durationDays * 2) % 68, 4, 72);
|
||||||
|
const width = clamp(20 + durationDays * 5, 22, 48);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: issue.id,
|
||||||
|
label: issue.name,
|
||||||
|
subtitle: `${project.identifier}-${issue.sequence_id ?? index + 1}`,
|
||||||
|
start,
|
||||||
|
width,
|
||||||
|
tone: index % 3 === 0 ? "accent" : index % 3 === 1 ? "white" : "muted",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function HomeGanttPreview(props: HomeGanttPreviewProps) {
|
||||||
|
const { analytics, project, workspaceSlug } = props;
|
||||||
|
const { currentLocale } = useTranslation();
|
||||||
|
const [activeRange, setActiveRange] = useState<TGanttRange>("Live");
|
||||||
|
const [isCompactMode, setIsCompactMode] = useState(false);
|
||||||
|
const [isFilterActive, setIsFilterActive] = useState(false);
|
||||||
|
|
||||||
|
const { data: issueResponse, isLoading } = useSWR(
|
||||||
|
project ? `HOME_GANTT_PREVIEW_${workspaceSlug}_${project.id}` : null,
|
||||||
|
project
|
||||||
|
? () =>
|
||||||
|
issueService.getIssues(workspaceSlug, project.id, {
|
||||||
|
order_by: "target_date",
|
||||||
|
per_page: GANTT_PREVIEW_LIMIT.toString(),
|
||||||
|
cursor: GANTT_PREVIEW_CURSOR,
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
{
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const timelineLabels = useMemo(() => {
|
||||||
|
const locale = currentLocale || "ru-RU";
|
||||||
|
const dayFormatter = new Intl.DateTimeFormat(locale, {
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
});
|
||||||
|
const hourFormatter = new Intl.DateTimeFormat(locale, {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activeRange === "Live" || activeRange === "1D") {
|
||||||
|
const date = new Date();
|
||||||
|
const step = activeRange === "Live" ? 2 : 3;
|
||||||
|
return Array.from({ length: activeRange === "Live" ? 8 : 9 }, (_, index) => {
|
||||||
|
const labelDate = new Date(date);
|
||||||
|
labelDate.setHours(date.getHours() + index * step);
|
||||||
|
return hourFormatter.format(labelDate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from({ length: activeRange === "1W" ? 7 : 8 }, (_, index) => {
|
||||||
|
const date = new Date();
|
||||||
|
date.setDate(date.getDate() + index * (activeRange === "1W" ? 1 : 4));
|
||||||
|
return dayFormatter.format(date);
|
||||||
|
});
|
||||||
|
}, [activeRange, currentLocale]);
|
||||||
|
|
||||||
|
const previewItems = useMemo(() => {
|
||||||
|
const issues = issueResponse?.results;
|
||||||
|
if (project && Array.isArray(issues) && issues.length > 0) return buildIssueItems(issues, project);
|
||||||
|
return buildSyntheticItems(project, analytics);
|
||||||
|
}, [analytics, issueResponse, project]);
|
||||||
|
|
||||||
|
const visibleItems = isFilterActive ? previewItems.filter((item) => item.tone !== "muted") : previewItems;
|
||||||
|
const timelineWidth = `${Math.max(timelineLabels.length * 168 + 240, 1080)}px`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="nodedc-home-gantt-card">
|
||||||
|
<div className="nodedc-home-gantt-toolbar">
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<div className="grid size-10 shrink-0 place-items-center rounded-full bg-black text-[rgb(var(--nodedc-card-active-rgb))]">
|
||||||
|
<CalendarDays className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-18 leading-none font-semibold text-primary">Календарное окно Ганта</div>
|
||||||
|
<div className="mt-1 truncate text-12 text-secondary">
|
||||||
|
{project ? `${project.name} / ближайший рабочий горизонт` : "Выберите проект для живого окна"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{GANTT_RANGES.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={activeRange === item}
|
||||||
|
className={cn("nodedc-home-gantt-chip", { "nodedc-home-gantt-chip-active": activeRange === item })}
|
||||||
|
onClick={() => setActiveRange(item)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn("nodedc-home-gantt-round-button", {
|
||||||
|
"nodedc-home-gantt-round-button-active": isCompactMode,
|
||||||
|
})}
|
||||||
|
aria-pressed={isCompactMode}
|
||||||
|
aria-label="Плотный режим Ганта"
|
||||||
|
onClick={() => setIsCompactMode((value) => !value)}
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="size-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn("nodedc-home-gantt-round-button", {
|
||||||
|
"nodedc-home-gantt-round-button-active": isFilterActive,
|
||||||
|
})}
|
||||||
|
aria-pressed={isFilterActive}
|
||||||
|
aria-label="Фильтры Ганта"
|
||||||
|
onClick={() => setIsFilterActive((value) => !value)}
|
||||||
|
>
|
||||||
|
<Filter className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="nodedc-home-gantt-surface">
|
||||||
|
<div className="nodedc-home-gantt-scroll" tabIndex={0} aria-label="Горизонтальная прокрутка окна Ганта">
|
||||||
|
<div className="nodedc-home-gantt-canvas" style={{ width: timelineWidth }}>
|
||||||
|
<div
|
||||||
|
className="nodedc-home-gantt-grid"
|
||||||
|
style={{ gridTemplateColumns: `repeat(${timelineLabels.length}, minmax(10rem, 1fr))` }}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{timelineLabels.map((label, index) => (
|
||||||
|
<div key={`${label}-${index}`} className="nodedc-home-gantt-grid-column">
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn("relative z-[1] space-y-3 pt-12", { "space-y-2": isCompactMode })}>
|
||||||
|
{isLoading
|
||||||
|
? Array.from({ length: 4 }, (_, index) => (
|
||||||
|
<div key={index} className="h-12 animate-pulse rounded-[1.25rem] bg-white/5" />
|
||||||
|
))
|
||||||
|
: visibleItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={cn("nodedc-home-gantt-row", { "nodedc-home-gantt-row-compact": isCompactMode })}
|
||||||
|
>
|
||||||
|
<div className="nodedc-home-gantt-row-label min-w-0">
|
||||||
|
<div className="truncate text-12 font-semibold text-primary">{item.label}</div>
|
||||||
|
<div className="mt-0.5 truncate text-11 text-placeholder">{item.subtitle}</div>
|
||||||
|
</div>
|
||||||
|
<div className="nodedc-home-gantt-track">
|
||||||
|
<div
|
||||||
|
className={cn("nodedc-home-gantt-bar", `nodedc-home-gantt-bar-${item.tone}`)}
|
||||||
|
style={{
|
||||||
|
left: `${item.start}%`,
|
||||||
|
width: `${item.width}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,9 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useId, useMemo } from "react";
|
import { type ReactNode, useId, useMemo } from "react";
|
||||||
import { Activity, CheckCircle2, Layers3, UsersRound } from "lucide-react";
|
import { Activity, CheckCircle2, Layers3, UsersRound } from "lucide-react";
|
||||||
import type { TActivityEntityData, TProjectAnalyticsCount } from "@plane/types";
|
import type { TActivityEntityData, TProjectAnalyticsCount } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
import { HomeCardShell } from "./home-card-shell";
|
|
||||||
import {
|
import {
|
||||||
aggregateProjectAnalytics,
|
aggregateProjectAnalytics,
|
||||||
getActivityProjectId,
|
getActivityProjectId,
|
||||||
|
|
@ -22,6 +20,7 @@ type HomeProjectInsightsProps = {
|
||||||
analyticsCollection?: TProjectAnalyticsCount[];
|
analyticsCollection?: TProjectAnalyticsCount[];
|
||||||
recents?: TActivityEntityData[];
|
recents?: TActivityEntityData[];
|
||||||
locale: string;
|
locale: string;
|
||||||
|
recentActivitySlot?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TActivityPoint = {
|
type TActivityPoint = {
|
||||||
|
|
@ -33,10 +32,10 @@ type TActivityPoint = {
|
||||||
const formatCompactNumber = (value: number) => new Intl.NumberFormat("ru-RU", { notation: "compact" }).format(value);
|
const formatCompactNumber = (value: number) => new Intl.NumberFormat("ru-RU", { notation: "compact" }).format(value);
|
||||||
|
|
||||||
const buildChartPaths = (data: TActivityPoint[]) => {
|
const buildChartPaths = (data: TActivityPoint[]) => {
|
||||||
const width = 420;
|
const width = 720;
|
||||||
const height = 180;
|
const height = 220;
|
||||||
const paddingX = 10;
|
const paddingX = 24;
|
||||||
const paddingY = 18;
|
const paddingY = 26;
|
||||||
const maxValue = Math.max(...data.map((item) => item.value), 1);
|
const maxValue = Math.max(...data.map((item) => item.value), 1);
|
||||||
const stepX = data.length > 1 ? (width - paddingX * 2) / (data.length - 1) : 0;
|
const stepX = data.length > 1 ? (width - paddingX * 2) / (data.length - 1) : 0;
|
||||||
|
|
||||||
|
|
@ -46,17 +45,28 @@ const buildChartPaths = (data: TActivityPoint[]) => {
|
||||||
return { x, y };
|
return { x, y };
|
||||||
});
|
});
|
||||||
|
|
||||||
const linePath = points
|
const linePath = points.reduce((path, point, index) => {
|
||||||
.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x.toFixed(2)} ${point.y.toFixed(2)}`)
|
if (index === 0) return `M ${point.x.toFixed(2)} ${point.y.toFixed(2)}`;
|
||||||
.join(" ");
|
|
||||||
|
const previousPoint = points[index - 1];
|
||||||
|
const controlDistance = (point.x - previousPoint.x) * 0.44;
|
||||||
|
|
||||||
|
return `${path} C ${(previousPoint.x + controlDistance).toFixed(2)} ${previousPoint.y.toFixed(2)}, ${(
|
||||||
|
point.x - controlDistance
|
||||||
|
).toFixed(2)} ${point.y.toFixed(2)}, ${point.x.toFixed(2)} ${point.y.toFixed(2)}`;
|
||||||
|
}, "");
|
||||||
const areaPath = `${linePath} L ${(points[points.length - 1]?.x ?? paddingX).toFixed(2)} ${(
|
const areaPath = `${linePath} L ${(points[points.length - 1]?.x ?? paddingX).toFixed(2)} ${(
|
||||||
height - paddingY
|
height - paddingY
|
||||||
).toFixed(2)} L ${(points[0]?.x ?? paddingX).toFixed(2)} ${(height - paddingY).toFixed(2)} Z`;
|
).toFixed(2)} L ${(points[0]?.x ?? paddingX).toFixed(2)} ${(height - paddingY).toFixed(2)} Z`;
|
||||||
|
const pointPercents = points.map((point) => ({
|
||||||
|
left: (point.x / width) * 100,
|
||||||
|
top: (point.y / height) * 100,
|
||||||
|
}));
|
||||||
|
|
||||||
return { width, height, paddingY, points, areaPath, linePath, maxValue };
|
return { width, height, paddingY, points, pointPercents, areaPath, linePath, maxValue };
|
||||||
};
|
};
|
||||||
|
|
||||||
export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
const useHomeProjectInsightData = (props: HomeProjectInsightsProps) => {
|
||||||
const { project, analytics, analyticsCollection, recents, locale } = props;
|
const { project, analytics, analyticsCollection, recents, locale } = props;
|
||||||
const chartId = useId();
|
const chartId = useId();
|
||||||
|
|
||||||
|
|
@ -155,61 +165,58 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return {
|
||||||
<HomeCardShell
|
activitySeries,
|
||||||
eyebrow={project ? "Фокус проекта" : "Workspace overview"}
|
chart,
|
||||||
title={project ? project.name : "Координационный обзор workspace"}
|
chartId,
|
||||||
description={
|
completedIssues,
|
||||||
project
|
completionRate,
|
||||||
? `${project.identifier} ${project.description ? `• ${project.description}` : "• домашняя сводка проекта в одном экране"}`
|
metricCards,
|
||||||
: "Агрегированный обзор по текущим проектам, recent activity и операционной нагрузке."
|
openIssues,
|
||||||
}
|
progressRows,
|
||||||
tone="default"
|
project,
|
||||||
>
|
recentTouchpoints,
|
||||||
<div className="grid gap-5 xl:grid-cols-[minmax(0,1.2fr)_minmax(260px,0.8fr)]">
|
totalIssues,
|
||||||
<div className="space-y-5">
|
};
|
||||||
<div className="grid gap-3 md:grid-cols-3">
|
};
|
||||||
{metricCards.map((metric) => (
|
|
||||||
<div
|
|
||||||
key={metric.label}
|
|
||||||
className={cn("nodedc-home-metric-card", { "nodedc-home-metric-card-accent": metric.accent })}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div className="text-12 font-medium text-secondary">{metric.label}</div>
|
|
||||||
<div className="text-[rgb(var(--nodedc-accent-rgb))]">{metric.icon}</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 text-28 font-semibold text-primary">{metric.value}</div>
|
|
||||||
<div className="mt-1 text-12 text-secondary">{metric.caption}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="nodedc-home-chart-panel">
|
export function HomeActivityTrendCard(props: HomeProjectInsightsProps) {
|
||||||
<div className="mb-4 flex items-center justify-between gap-3">
|
const { activitySeries, chart, chartId, project, recentTouchpoints } = useHomeProjectInsightData(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="nodedc-home-focus-card nodedc-home-activity-card">
|
||||||
|
<div className="relative z-[1] p-5 xl:p-6">
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-14 font-semibold text-primary">Темп активности</div>
|
<div className="text-[11px] font-semibold tracking-[0.22em] text-placeholder uppercase">
|
||||||
<div className="text-12 text-secondary">Последние 7 дней переходов и взаимодействий внутри сводки.</div>
|
{project?.identifier ?? "Workspace"}
|
||||||
</div>
|
</div>
|
||||||
<div className="nodedc-home-soft-badge rounded-full px-3 py-1.5 text-12 text-secondary">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
{recentTouchpoints} событий
|
<Activity className="size-4 text-[rgb(var(--nodedc-accent-rgb))]" />
|
||||||
|
<div className="text-18 font-semibold text-primary">Темп активности</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-12 text-secondary">Последние 7 дней переходов и взаимодействий.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="nodedc-home-focus-chip">{recentTouchpoints} событий</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-home-subpanel relative overflow-hidden p-4">
|
<div className="nodedc-home-chart-panel relative overflow-hidden">
|
||||||
<div className="absolute inset-x-6 top-4 bottom-4 grid grid-cols-4 gap-4 opacity-25">
|
<div className="absolute inset-x-0 top-4 bottom-16 grid grid-cols-6 gap-4 opacity-30">
|
||||||
{["col-1", "col-2", "col-3", "col-4"].map((key) => (
|
{["col-1", "col-2", "col-3", "col-4", "col-5", "col-6"].map((key) => (
|
||||||
<div key={key} className="border-r border-dashed border-white/6 last:border-r-0" />
|
<div key={key} className="border-r border-dashed border-white/[0.06] last:border-r-0" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
viewBox={`0 0 ${chart.width} ${chart.height}`}
|
viewBox={`0 0 ${chart.width} ${chart.height}`}
|
||||||
className="relative z-[1] h-[180px] w-full"
|
className="relative z-[1] h-[220px] w-full"
|
||||||
preserveAspectRatio="none"
|
preserveAspectRatio="none"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id={`${chartId}-fill`} x1="0" x2="0" y1="0" y2="1">
|
<linearGradient id={`${chartId}-fill`} x1="0" x2="0" y1="0" y2="1">
|
||||||
<stop offset="0%" stopColor="rgba(var(--nodedc-accent-rgb),0.64)" />
|
<stop offset="0%" stopColor="rgba(var(--nodedc-accent-rgb),0.28)" />
|
||||||
<stop offset="100%" stopColor="rgba(var(--nodedc-accent-rgb),0.02)" />
|
<stop offset="100%" stopColor="rgba(var(--nodedc-accent-rgb),0.02)" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
@ -222,7 +229,7 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
x2={x}
|
x2={x}
|
||||||
y1={12}
|
y1={12}
|
||||||
y2={chart.height - chart.paddingY}
|
y2={chart.height - chart.paddingY}
|
||||||
stroke="rgba(255,255,255,0.05)"
|
stroke="rgba(255,255,255,0.04)"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -235,7 +242,7 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
x2={chart.width - 10}
|
x2={chart.width - 10}
|
||||||
y1={y}
|
y1={y}
|
||||||
y2={y}
|
y2={y}
|
||||||
stroke="rgba(255,255,255,0.05)"
|
stroke="rgba(255,255,255,0.06)"
|
||||||
strokeDasharray="4 6"
|
strokeDasharray="4 6"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -245,30 +252,34 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
d={chart.linePath}
|
d={chart.linePath}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="rgb(var(--nodedc-accent-rgb))"
|
stroke="rgb(var(--nodedc-accent-rgb))"
|
||||||
strokeWidth="4"
|
strokeWidth="5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div className="pointer-events-none absolute inset-x-4 top-4 z-[2] h-[220px]">
|
||||||
{activitySeries.map((activityPoint, index) => {
|
{activitySeries.map((activityPoint, index) => {
|
||||||
const point = chart.points[index];
|
const point = chart.pointPercents[index];
|
||||||
if (!point) return null;
|
if (!point) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<circle
|
<span
|
||||||
key={activityPoint.key}
|
key={activityPoint.key}
|
||||||
cx={point.x}
|
className="absolute size-2.5 rounded-full bg-[rgb(var(--nodedc-card-active-rgb))] shadow-[0_0_0_3px_rgba(8,8,10,0.82)]"
|
||||||
cy={point.y}
|
style={{
|
||||||
r="5"
|
left: `${point.left}%`,
|
||||||
fill="rgb(var(--nodedc-accent-rgb))"
|
top: `${point.top}%`,
|
||||||
stroke="rgba(9,9,12,0.8)"
|
transform: "translate(-50%, -50%)",
|
||||||
strokeWidth="3"
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</svg>
|
</div>
|
||||||
|
|
||||||
<div className="relative z-[1] mt-4 grid grid-cols-7 gap-2">
|
<div className="mt-4 grid grid-cols-7 gap-2">
|
||||||
{activitySeries.map((point) => (
|
{activitySeries.map((point) => (
|
||||||
<div key={point.key} className="nodedc-home-soft-badge rounded-2xl px-2 py-2 text-center">
|
<div key={point.key} className="text-center">
|
||||||
<div className="text-[11px] tracking-[0.14em] text-placeholder uppercase">{point.label}</div>
|
<div className="text-[11px] tracking-[0.14em] text-placeholder uppercase">{point.label}</div>
|
||||||
<div className="mt-1 text-13 font-semibold text-primary">{point.value}</div>
|
<div className="mt-1 text-13 font-semibold text-primary">{point.value}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -277,93 +288,146 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HomeOperationsOverview(props: HomeProjectInsightsProps) {
|
||||||
|
const { recentActivitySlot } = props;
|
||||||
|
const {
|
||||||
|
completedIssues,
|
||||||
|
completionRate,
|
||||||
|
metricCards,
|
||||||
|
openIssues,
|
||||||
|
progressRows,
|
||||||
|
project,
|
||||||
|
recentTouchpoints,
|
||||||
|
totalIssues,
|
||||||
|
} = useHomeProjectInsightData(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="grid gap-4 xl:grid-cols-[minmax(0,0.95fr)_minmax(0,0.95fr)_minmax(300px,1.1fr)]">
|
||||||
|
<div className="nodedc-home-subpanel space-y-4 p-5">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="grid size-11 place-items-center rounded-2xl bg-[rgba(var(--nodedc-accent-rgb),0.14)] text-[rgb(var(--nodedc-accent-rgb))]">
|
||||||
|
<UsersRound className="size-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-15 font-semibold text-primary">Операционный срез</div>
|
||||||
|
<div className="text-12 text-secondary">Команда, циклы и модули относительно текущего workspace.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="nodedc-home-chart-panel p-5">
|
{progressRows.map((row) => {
|
||||||
<div className="flex items-center gap-3">
|
const percent = row.max > 0 ? Math.max((row.value / row.max) * 100, row.value > 0 ? 10 : 0) : 0;
|
||||||
<div className="grid size-11 place-items-center rounded-2xl bg-[rgba(0,0,0,0.28)] text-[rgb(var(--nodedc-accent-rgb))]">
|
|
||||||
<UsersRound className="size-5" />
|
return (
|
||||||
</div>
|
<div key={row.label} className="space-y-2">
|
||||||
<div>
|
<div className="flex items-center justify-between gap-3 text-12">
|
||||||
<div className="text-13 font-semibold text-primary">Операционный срез</div>
|
<span className="text-secondary">{row.label}</span>
|
||||||
<div className="text-12 text-secondary">
|
<span className="font-semibold text-primary">{row.value}</span>
|
||||||
Нагрузка команды, циклов и модулей относительно остального workspace.
|
</div>
|
||||||
|
<div className="nodedc-home-focus-track">
|
||||||
|
<div className="nodedc-home-focus-fill" style={{ width: `${Math.min(percent, 100)}%` }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="nodedc-home-subpanel space-y-4 p-5">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-15 font-semibold text-primary">Ритм исполнения</div>
|
||||||
|
<div className="text-12 text-secondary">Закрытый объём и открытый остаток по фокусу.</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid size-11 place-items-center rounded-full bg-black text-[rgb(var(--nodedc-card-active-rgb))]">
|
||||||
|
<CheckCircle2 className="size-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{metricCards.map((metric) => (
|
||||||
|
<div key={metric.label} className="rounded-[1.15rem] bg-black/[0.12] p-3">
|
||||||
|
<div className="text-11 leading-4 text-secondary">{metric.label}</div>
|
||||||
|
<div className="mt-2 text-18 leading-none font-semibold text-primary">{metric.value}</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 space-y-4">
|
<div className="space-y-4">
|
||||||
{progressRows.map((row) => {
|
<div className="space-y-2">
|
||||||
const percent = row.max > 0 ? Math.max((row.value / row.max) * 100, row.value > 0 ? 10 : 0) : 0;
|
<div className="flex items-center justify-between gap-3 text-12">
|
||||||
|
<span className="text-secondary">Закрытые задачи</span>
|
||||||
return (
|
<span className="font-semibold text-primary">{completedIssues}</span>
|
||||||
<div key={row.label} className="space-y-2">
|
</div>
|
||||||
<div className="flex items-center justify-between gap-3 text-12">
|
<div className="nodedc-home-focus-track">
|
||||||
<span className="text-secondary">{row.label}</span>
|
<div
|
||||||
<span className="font-semibold text-primary">{row.value}</span>
|
className="nodedc-home-focus-fill"
|
||||||
</div>
|
style={{ width: `${totalIssues > 0 ? (completedIssues / totalIssues) * 100 : 0}%` }}
|
||||||
<div className="nodedc-home-progress-track">
|
/>
|
||||||
<div className="nodedc-home-progress-fill" style={{ width: `${Math.min(percent, 100)}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-home-chart-panel p-5">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3 text-12">
|
||||||
<div>
|
<span className="text-secondary">Открытый остаток</span>
|
||||||
<div className="text-13 font-semibold text-primary">Ритм исполнения</div>
|
<span className="font-semibold text-primary">{openIssues}</span>
|
||||||
<div className="text-12 text-secondary">Сколько уже закрыто и какой объём ещё держим открытым.</div>
|
|
||||||
</div>
|
|
||||||
<div className="nodedc-home-soft-badge rounded-full px-3 py-1.5 text-12 text-secondary">
|
|
||||||
{completionRate}%
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="nodedc-home-focus-track">
|
||||||
<div className="mt-5 space-y-4">
|
<div
|
||||||
<div className="nodedc-home-subpanel p-4">
|
className="nodedc-home-focus-fill opacity-65"
|
||||||
<div className="flex items-center justify-between gap-3 text-12">
|
style={{
|
||||||
<span className="text-secondary">Закрытые задачи</span>
|
width: `${totalIssues > 0 ? (openIssues / totalIssues) * 100 : 0}%`,
|
||||||
<span className="font-semibold text-primary">{completedIssues}</span>
|
height: "100%",
|
||||||
</div>
|
}}
|
||||||
<div className="nodedc-home-progress-track mt-3">
|
/>
|
||||||
<div
|
|
||||||
className="nodedc-home-progress-fill"
|
|
||||||
style={{ width: `${totalIssues > 0 ? (completedIssues / totalIssues) * 100 : 0}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="nodedc-home-subpanel p-4">
|
|
||||||
<div className="flex items-center justify-between gap-3 text-12">
|
|
||||||
<span className="text-secondary">Открытый остаток</span>
|
|
||||||
<span className="font-semibold text-primary">{openIssues}</span>
|
|
||||||
</div>
|
|
||||||
<div className="nodedc-home-progress-track mt-3">
|
|
||||||
<div
|
|
||||||
className="rounded-full bg-white/16"
|
|
||||||
style={{
|
|
||||||
width: `${totalIssues > 0 ? (openIssues / totalIssues) * 100 : 0}%`,
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="nodedc-home-subpanel p-4 text-12 text-secondary">
|
|
||||||
<span className="font-semibold text-primary">{project ? project.identifier : "Workspace"}</span>
|
|
||||||
<span> держит </span>
|
|
||||||
<span className="font-semibold text-primary">{totalIssues}</span>
|
|
||||||
<span> задач в общей матрице и </span>
|
|
||||||
<span className="font-semibold text-primary">{recentTouchpoints}</span>
|
|
||||||
<span> недавних касаний за неделю.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="text-12 leading-5 text-secondary">
|
||||||
|
<span className="font-semibold text-primary">{project ? project.identifier : "Workspace"}</span>
|
||||||
|
<span> держит </span>
|
||||||
|
<span className="font-semibold text-primary">{totalIssues}</span>
|
||||||
|
<span> задач и </span>
|
||||||
|
<span className="font-semibold text-primary">{recentTouchpoints}</span>
|
||||||
|
<span> недавних касаний.</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HomeCardShell>
|
|
||||||
|
<div className="nodedc-home-subpanel p-5">
|
||||||
|
{recentActivitySlot ? (
|
||||||
|
<div className="h-full min-h-[22rem]">{recentActivitySlot}</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full min-h-[22rem] flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="grid size-11 place-items-center rounded-full bg-black text-[rgb(var(--nodedc-card-active-rgb))]">
|
||||||
|
<Layers3 className="size-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-15 font-semibold text-primary">Недавние</div>
|
||||||
|
<div className="text-12 text-secondary">Виджет recent activity отключен в настройках.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="nodedc-home-focus-chip w-fit">{completionRate}% готовность</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-5">
|
||||||
|
<HomeActivityTrendCard {...props} />
|
||||||
|
<HomeOperationsOverview {...props} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Link from "next/link";
|
import { FolderOpenDot, Layers3, Search, UsersRound } from "lucide-react";
|
||||||
import { ArrowUpRight, FolderOpenDot, Layers3, UsersRound } from "lucide-react";
|
|
||||||
import type { TActivityEntityData, TProjectAnalyticsCount } from "@plane/types";
|
import type { TActivityEntityData, TProjectAnalyticsCount } from "@plane/types";
|
||||||
import { Logo } from "@plane/propel/emoji-icon-picker";
|
import { Logo } from "@plane/propel/emoji-icon-picker";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
|
@ -14,20 +13,29 @@ import { HomeCardShell } from "./home-card-shell";
|
||||||
import { getActivityProjectId, getCompletionRate, type THomeProjectData } from "./home.utils";
|
import { getActivityProjectId, getCompletionRate, type THomeProjectData } from "./home.utils";
|
||||||
|
|
||||||
type HomeProjectStackProps = {
|
type HomeProjectStackProps = {
|
||||||
|
className?: string;
|
||||||
projects: THomeProjectData[];
|
projects: THomeProjectData[];
|
||||||
analyticsMap: Record<string, TProjectAnalyticsCount | undefined>;
|
analyticsMap: Record<string, TProjectAnalyticsCount | undefined>;
|
||||||
|
orientation?: "horizontal" | "vertical";
|
||||||
recents?: TActivityEntityData[];
|
recents?: TActivityEntityData[];
|
||||||
selectedProjectId: string | null;
|
selectedProjectId: string | null;
|
||||||
onSelectProject: (projectId: string) => void;
|
onSelectProject: (projectId: string) => void;
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const STACK_VISIBLE_LIMIT = 4;
|
const STACK_VISIBLE_LIMIT = 4;
|
||||||
const ACTIVE_CARD_HEIGHT = 228;
|
const ACTIVE_CARD_HEIGHT = 248;
|
||||||
const STACK_OFFSET = 76;
|
const STACK_OFFSET = 88;
|
||||||
|
|
||||||
export function HomeProjectStack(props: HomeProjectStackProps) {
|
export function HomeProjectStack(props: HomeProjectStackProps) {
|
||||||
const { projects, analyticsMap, recents, selectedProjectId, onSelectProject, workspaceSlug } = props;
|
const {
|
||||||
|
className,
|
||||||
|
projects,
|
||||||
|
analyticsMap,
|
||||||
|
orientation = "vertical",
|
||||||
|
recents,
|
||||||
|
selectedProjectId,
|
||||||
|
onSelectProject,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const activeProject = projects.find((project: THomeProjectData) => project.id === selectedProjectId);
|
const activeProject = projects.find((project: THomeProjectData) => project.id === selectedProjectId);
|
||||||
const orderedProjects = activeProject
|
const orderedProjects = activeProject
|
||||||
|
|
@ -45,19 +53,14 @@ export function HomeProjectStack(props: HomeProjectStackProps) {
|
||||||
|
|
||||||
const selectedProject =
|
const selectedProject =
|
||||||
orderedProjects.find((project: THomeProjectData) => project.id === selectedProjectId) ?? orderedProjects[0];
|
orderedProjects.find((project: THomeProjectData) => project.id === selectedProjectId) ?? orderedProjects[0];
|
||||||
const selectedProjectPath = selectedProject ? `/${workspaceSlug}/projects/${selectedProject.id}/issues` : null;
|
|
||||||
const stackHeight =
|
const stackHeight =
|
||||||
visibleProjects.length > 0 ? ACTIVE_CARD_HEIGHT + (visibleProjects.length - 1) * STACK_OFFSET : 228;
|
visibleProjects.length > 0 ? ACTIVE_CARD_HEIGHT + (visibleProjects.length - 1) * STACK_OFFSET : 228;
|
||||||
|
const isHorizontal = orientation === "horizontal";
|
||||||
|
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
return (
|
return (
|
||||||
<HomeCardShell
|
<HomeCardShell eyebrow="Projects" title="Доступные проекты" description="Пока список проектов пуст.">
|
||||||
eyebrow="Workspace"
|
<div className="rounded-[26px] bg-black/10 p-5">
|
||||||
title="Доступные проекты"
|
|
||||||
description="Когда проекты появятся в workspace, здесь появится интерактивный стек для быстрого переключения домашней сводки."
|
|
||||||
tone="accent"
|
|
||||||
>
|
|
||||||
<div className="rounded-[26px] border border-white/8 bg-black/10 p-5">
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="grid size-12 place-items-center rounded-2xl bg-[rgba(var(--nodedc-accent-rgb),0.18)] text-[rgb(var(--nodedc-accent-rgb))]">
|
<div className="grid size-12 place-items-center rounded-2xl bg-[rgba(var(--nodedc-accent-rgb),0.18)] text-[rgb(var(--nodedc-accent-rgb))]">
|
||||||
<FolderOpenDot className="size-5" />
|
<FolderOpenDot className="size-5" />
|
||||||
|
|
@ -74,160 +77,169 @@ export function HomeProjectStack(props: HomeProjectStackProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const renderProjectCard = (project: THomeProjectData, index: number, horizontal: boolean) => {
|
||||||
<HomeCardShell
|
const analytics = analyticsMap[project.id];
|
||||||
eyebrow="Workspace"
|
const completionRate = getCompletionRate(analytics);
|
||||||
title="Доступные проекты"
|
const totalIssues = analytics?.total_issues ?? 0;
|
||||||
description="Нажатие на карточку проекта перестраивает домашнюю сводку, recent activity и аналитический фокус справа."
|
const completedIssues = analytics?.completed_issues ?? 0;
|
||||||
tone="accent"
|
const activeItems = Math.max(totalIssues - completedIssues, 0);
|
||||||
action={
|
const activityCount = activityCountByProject[project.id] ?? 0;
|
||||||
selectedProjectPath ? (
|
const isActive = project.id === selectedProject?.id;
|
||||||
<Link href={selectedProjectPath} className="nodedc-toolbar-pill inline-flex items-center gap-2">
|
|
||||||
<span>Открыть проект</span>
|
|
||||||
<ArrowUpRight className="size-4" />
|
|
||||||
</Link>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="relative" style={{ height: `${stackHeight}px` }}>
|
|
||||||
{visibleProjects.map((project: THomeProjectData, index: number) => {
|
|
||||||
const analytics = analyticsMap[project.id];
|
|
||||||
const completionRate = getCompletionRate(analytics);
|
|
||||||
const totalIssues = analytics?.total_issues ?? 0;
|
|
||||||
const completedIssues = analytics?.completed_issues ?? 0;
|
|
||||||
const activeItems = Math.max(totalIssues - completedIssues, 0);
|
|
||||||
const activityCount = activityCountByProject[project.id] ?? 0;
|
|
||||||
const isActive = project.id === selectedProject?.id;
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={project.id}
|
||||||
|
type="button"
|
||||||
|
className={cn("nodedc-home-project-card text-left", {
|
||||||
|
"nodedc-home-project-card-horizontal shrink-0": horizontal,
|
||||||
|
"absolute inset-x-0": !horizontal,
|
||||||
|
"cursor-default": isActive,
|
||||||
|
})}
|
||||||
|
data-active={isActive}
|
||||||
|
onClick={() => onSelectProject(project.id)}
|
||||||
|
style={
|
||||||
|
horizontal
|
||||||
|
? { zIndex: visibleProjects.length - index }
|
||||||
|
: {
|
||||||
|
top: `${index * STACK_OFFSET}px`,
|
||||||
|
zIndex: visibleProjects.length - index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CoverImage
|
||||||
|
src={project.cover_image_url}
|
||||||
|
alt={project.name}
|
||||||
|
showDefaultWhenEmpty
|
||||||
|
className="absolute inset-0 h-full w-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(var(--nodedc-accent-rgb),0.28),transparent_34%),linear-gradient(160deg,rgba(5,5,8,0.08)_0%,rgba(5,5,8,0.42)_34%,rgba(5,5,8,0.84)_100%)]" />
|
||||||
|
<div className="relative flex h-full flex-col justify-between p-4">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-full bg-black/25 px-2.5 py-1 text-[11px] font-medium text-white/[0.72] backdrop-blur-md">
|
||||||
|
<Logo logo={project.logo_props} size={14} />
|
||||||
|
<span>{project.identifier}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-2.5 py-1 text-[11px] font-semibold backdrop-blur-md",
|
||||||
|
isActive
|
||||||
|
? "bg-[rgba(var(--nodedc-accent-rgb),0.82)] text-[rgb(var(--nodedc-on-card-active-rgb))]"
|
||||||
|
: "bg-white/[0.12] text-white/[0.72]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isActive ? "В фокусе" : `${completionRate}%`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-18 font-semibold text-white">{project.name}</div>
|
||||||
|
{project.description && isActive && (
|
||||||
|
<p className="line-clamp-2 max-w-[18rem] text-12 leading-5 text-white/[0.72]">{project.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
<div className="rounded-2xl bg-black/[0.24] px-3 py-2 backdrop-blur-md">
|
||||||
|
<div className="text-[11px] tracking-[0.18em] text-white/[0.45] uppercase">Открыто</div>
|
||||||
|
<div className="text-15 mt-1 font-semibold text-white">{activeItems}</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl bg-black/[0.24] px-3 py-2 backdrop-blur-md">
|
||||||
|
<div className="text-[11px] tracking-[0.18em] text-white/[0.45] uppercase">Закрыто</div>
|
||||||
|
<div className="text-15 mt-1 font-semibold text-white">{completedIssues}</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl bg-black/[0.24] px-3 py-2 backdrop-blur-md">
|
||||||
|
<div className="text-[11px] tracking-[0.18em] text-white/[0.45] uppercase">Касания</div>
|
||||||
|
<div className="text-15 mt-1 font-semibold text-white">{activityCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={cn("nodedc-home-card nodedc-home-project-panel flex flex-col px-4 py-4", className)}>
|
||||||
|
<div className="mb-4 flex items-center justify-between gap-3 px-1">
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-semibold tracking-[0.22em] text-placeholder uppercase">Quick Project</div>
|
||||||
|
<div className="mt-1 text-16 font-semibold text-primary">Выбор проекта</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" className="nodedc-home-gantt-round-button" aria-label="Поиск проекта">
|
||||||
|
<Search className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isHorizontal ? (
|
||||||
|
<div className="nodedc-home-project-deck-scroller">
|
||||||
|
<div className="nodedc-home-project-deck-row flex items-start px-1 py-2">
|
||||||
|
{visibleProjects.map((project: THomeProjectData, index: number) => renderProjectCard(project, index, true))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative" style={{ height: `${stackHeight}px` }}>
|
||||||
|
{visibleProjects.map((project: THomeProjectData, index: number) => renderProjectCard(project, index, false))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4 rounded-[24px] bg-black/10 p-4 xl:mt-auto">
|
||||||
|
<div className="mb-3 flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-13 font-semibold text-primary">Быстрый выбор</div>
|
||||||
|
<div className="text-12 text-secondary">Все проекты пользователя в текущем workspace.</div>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-full bg-white/[0.06] px-3 py-1.5 text-12 text-secondary">
|
||||||
|
<Layers3 className="size-3.5" />
|
||||||
|
<span>{projects.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{orderedProjects.map((project: THomeProjectData) => {
|
||||||
|
const analytics = analyticsMap[project.id];
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={project.id}
|
key={project.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn("nodedc-home-project-card absolute inset-x-0 text-left", {
|
className={cn("nodedc-toolbar-pill inline-flex items-center gap-2", {
|
||||||
"cursor-default": isActive,
|
"!bg-[rgb(var(--nodedc-card-active-rgb))] !text-[rgb(var(--nodedc-on-card-active-rgb))]":
|
||||||
|
project.id === selectedProject?.id,
|
||||||
})}
|
})}
|
||||||
data-active={isActive}
|
|
||||||
onClick={() => onSelectProject(project.id)}
|
onClick={() => onSelectProject(project.id)}
|
||||||
style={{
|
|
||||||
top: `${index * STACK_OFFSET}px`,
|
|
||||||
zIndex: visibleProjects.length - index,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CoverImage
|
<Logo logo={project.logo_props} size={14} />
|
||||||
src={project.cover_image_url}
|
<span>{project.identifier}</span>
|
||||||
alt={project.name}
|
<span className="text-[11px] opacity-70">{getCompletionRate(analytics)}%</span>
|
||||||
showDefaultWhenEmpty
|
|
||||||
className="absolute inset-0 h-full w-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(var(--nodedc-accent-rgb),0.28),transparent_34%),linear-gradient(160deg,rgba(5,5,8,0.08)_0%,rgba(5,5,8,0.42)_34%,rgba(5,5,8,0.84)_100%)]" />
|
|
||||||
<div className="relative flex h-full flex-col justify-between p-4">
|
|
||||||
<div className="flex items-start justify-between gap-3">
|
|
||||||
<div className="inline-flex items-center gap-2 rounded-full bg-black/25 px-2.5 py-1 text-[11px] font-medium text-white/72 backdrop-blur-md">
|
|
||||||
<Logo logo={project.logo_props} size={14} />
|
|
||||||
<span>{project.identifier}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"rounded-full px-2.5 py-1 text-[11px] font-semibold backdrop-blur-md",
|
|
||||||
isActive
|
|
||||||
? "bg-[rgba(var(--nodedc-accent-rgb),0.82)] text-[rgb(var(--nodedc-on-card-active-rgb))]"
|
|
||||||
: "bg-white/12 text-white/72"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isActive ? "В фокусе" : `${completionRate}%`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-18 font-semibold text-white">{project.name}</div>
|
|
||||||
{project.description && isActive && (
|
|
||||||
<p className="line-clamp-2 max-w-[18rem] text-12 leading-5 text-white/72">
|
|
||||||
{project.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="rounded-2xl bg-black/24 px-3 py-2 backdrop-blur-md">
|
|
||||||
<div className="text-[11px] tracking-[0.18em] text-white/45 uppercase">Открыто</div>
|
|
||||||
<div className="text-15 mt-1 font-semibold text-white">{activeItems}</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-2xl bg-black/24 px-3 py-2 backdrop-blur-md">
|
|
||||||
<div className="text-[11px] tracking-[0.18em] text-white/45 uppercase">Закрыто</div>
|
|
||||||
<div className="text-15 mt-1 font-semibold text-white">{completedIssues}</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-2xl bg-black/24 px-3 py-2 backdrop-blur-md">
|
|
||||||
<div className="text-[11px] tracking-[0.18em] text-white/45 uppercase">Касания</div>
|
|
||||||
<div className="text-15 mt-1 font-semibold text-white">{activityCount}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-[24px] border border-white/6 bg-black/10 p-4">
|
{selectedProject && (
|
||||||
<div className="mb-3 flex items-center justify-between gap-3">
|
<div className="mt-4 grid grid-cols-2 gap-3 rounded-[22px] bg-white/[0.04] p-3 md:grid-cols-3">
|
||||||
<div>
|
<div className="rounded-2xl bg-black/10 px-3 py-2">
|
||||||
<div className="text-13 font-semibold text-primary">Быстрый выбор</div>
|
<div className="text-[11px] tracking-[0.18em] text-placeholder uppercase">Фокус</div>
|
||||||
<div className="text-12 text-secondary">Все проекты пользователя в текущем workspace.</div>
|
<div className="mt-1 text-13 font-semibold text-primary">{selectedProject.identifier}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex items-center gap-2 rounded-full bg-white/6 px-3 py-1.5 text-12 text-secondary">
|
<div className="rounded-2xl bg-black/10 px-3 py-2">
|
||||||
<Layers3 className="size-3.5" />
|
<div className="flex items-center gap-1 text-[11px] tracking-[0.18em] text-placeholder uppercase">
|
||||||
<span>{projects.length}</span>
|
<UsersRound className="size-3.5" />
|
||||||
|
<span>Команда</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-13 font-semibold text-primary">
|
||||||
|
{analyticsMap[selectedProject.id]?.total_members ?? 0}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl bg-black/10 px-3 py-2">
|
||||||
|
<div className="text-[11px] tracking-[0.18em] text-placeholder uppercase">Контур</div>
|
||||||
|
<div className="mt-1 text-13 font-semibold text-primary">
|
||||||
|
{activityCountByProject[selectedProject.id] ?? 0} касаний
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{orderedProjects.map((project: THomeProjectData) => {
|
|
||||||
const analytics = analyticsMap[project.id];
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={project.id}
|
|
||||||
type="button"
|
|
||||||
className={cn("nodedc-toolbar-pill inline-flex items-center gap-2", {
|
|
||||||
"!bg-[rgb(var(--nodedc-card-active-rgb))] !text-[rgb(var(--nodedc-on-card-active-rgb))]":
|
|
||||||
project.id === selectedProject?.id,
|
|
||||||
})}
|
|
||||||
onClick={() => onSelectProject(project.id)}
|
|
||||||
>
|
|
||||||
<Logo logo={project.logo_props} size={14} />
|
|
||||||
<span>{project.identifier}</span>
|
|
||||||
<span className="text-[11px] opacity-70">{getCompletionRate(analytics)}%</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedProject && (
|
|
||||||
<div className="mt-4 grid grid-cols-2 gap-3 rounded-[22px] bg-white/4 p-3 md:grid-cols-3">
|
|
||||||
<div className="rounded-2xl bg-black/10 px-3 py-2">
|
|
||||||
<div className="text-[11px] tracking-[0.18em] text-placeholder uppercase">Фокус</div>
|
|
||||||
<div className="mt-1 text-13 font-semibold text-primary">{selectedProject.identifier}</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-2xl bg-black/10 px-3 py-2">
|
|
||||||
<div className="flex items-center gap-1 text-[11px] tracking-[0.18em] text-placeholder uppercase">
|
|
||||||
<UsersRound className="size-3.5" />
|
|
||||||
<span>Команда</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-13 font-semibold text-primary">
|
|
||||||
{analyticsMap[selectedProject.id]?.total_members ?? 0}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-2xl bg-black/10 px-3 py-2">
|
|
||||||
<div className="text-[11px] tracking-[0.18em] text-placeholder uppercase">Контур</div>
|
|
||||||
<div className="mt-1 text-13 font-semibold text-primary">
|
|
||||||
{activityCountByProject[selectedProject.id] ?? 0} recent
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</HomeCardShell>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1537,6 +1537,350 @@
|
||||||
box-shadow 160ms ease;
|
box-shadow 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-home-hero {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
isolation: isolate;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
border-radius: 1.9rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(10, 10, 12, 0.74) !important;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.022) !important;
|
||||||
|
-webkit-backdrop-filter: blur(28px);
|
||||||
|
backdrop-filter: blur(28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.nodedc-home-hero {
|
||||||
|
padding: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-hero-time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.8rem;
|
||||||
|
min-height: 1.75rem;
|
||||||
|
padding: 0.15rem 0.35rem 0.85rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-hero-grid {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.nodedc-home-hero-grid {
|
||||||
|
grid-template-columns: minmax(320px, 360px) minmax(0, 1fr);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-hero-title-cell {
|
||||||
|
display: flex;
|
||||||
|
min-height: 8rem;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
border-radius: 1.7rem;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.022), rgba(255, 255, 255, 0.006)), rgba(0, 0, 0, 0.18);
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-hero-title-cell h1 {
|
||||||
|
max-width: 16rem;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
font-size: clamp(2rem, 3vw, 3.35rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 0.92;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-hero-title-cell p {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-hero-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 2.45rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
padding-inline: 1.15rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-hero-pill-active {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
color: #0b1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-date-line {
|
||||||
|
color: rgba(255, 255, 255, 0.42);
|
||||||
|
font-size: clamp(1.75rem, 4.6vw, 4.8rem);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-market-band {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 8rem;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 1.5rem;
|
||||||
|
border-radius: 1.7rem;
|
||||||
|
background: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||||
|
padding: 1.25rem;
|
||||||
|
color: rgb(var(--nodedc-on-card-active-rgb));
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.42),
|
||||||
|
0 18px 42px rgba(0, 0, 0, 0.18) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.nodedc-home-market-band {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
isolation: isolate;
|
||||||
|
min-height: 30rem;
|
||||||
|
border-radius: 2rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.026) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(8, 8, 10, 0.78) !important;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
|
||||||
|
-webkit-backdrop-filter: blur(28px);
|
||||||
|
backdrop-filter: blur(28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-toolbar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 2.35rem;
|
||||||
|
min-width: 2.85rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.07) !important;
|
||||||
|
padding-inline: 0.95rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-chip-active {
|
||||||
|
background: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||||
|
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-round-button {
|
||||||
|
display: inline-grid !important;
|
||||||
|
width: 2.5rem;
|
||||||
|
min-width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
place-items: center;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
border-radius: 999px !important;
|
||||||
|
background: rgba(0, 0, 0, 0.42) !important;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-round-button:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.58) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-round-button-active {
|
||||||
|
background: rgb(var(--nodedc-card-active-rgb)) !important;
|
||||||
|
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-surface {
|
||||||
|
position: relative;
|
||||||
|
min-height: 23.5rem;
|
||||||
|
margin: 0 1.25rem 1.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 1.75rem;
|
||||||
|
background: rgba(0, 0, 0, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-scroll {
|
||||||
|
min-height: 23.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
scrollbar-color: rgba(var(--nodedc-card-active-rgb), 0.65) rgba(255, 255, 255, 0.04);
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-scroll::-webkit-scrollbar {
|
||||||
|
height: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-scroll::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(var(--nodedc-card-active-rgb), 0.65);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-canvas {
|
||||||
|
position: relative;
|
||||||
|
min-height: 23.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px) 0 0 / 6rem 100%,
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px) 0 0 / 100% 4.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-floating {
|
||||||
|
position: absolute;
|
||||||
|
top: 3.2rem;
|
||||||
|
left: 4.2rem;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
max-width: min(34rem, calc(100% - 3rem));
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
border-radius: 1.6rem;
|
||||||
|
background: rgba(38, 38, 42, 0.92);
|
||||||
|
padding: 1.25rem;
|
||||||
|
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.18);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-grid {
|
||||||
|
position: absolute;
|
||||||
|
inset: 1rem 1rem 1rem 11.5rem;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-grid-column {
|
||||||
|
min-height: 22rem;
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
color: var(--text-color-placeholder);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(7.5rem, 10rem) minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
min-height: 3.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-row-compact {
|
||||||
|
min-height: 2.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-row-label {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: linear-gradient(90deg, rgba(7, 7, 9, 0.94) 0%, rgba(7, 7, 9, 0.72) 76%, transparent 100%);
|
||||||
|
padding: 0.45rem 0.75rem 0.45rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-track {
|
||||||
|
position: relative;
|
||||||
|
height: 2.1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.35rem;
|
||||||
|
height: 1.4rem;
|
||||||
|
min-width: 2.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-bar-accent {
|
||||||
|
background: rgb(var(--nodedc-card-active-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-bar-white {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-bar-muted {
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.nodedc-home-gantt-card {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-toolbar {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-surface {
|
||||||
|
min-height: 24rem;
|
||||||
|
margin: 0 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-scroll,
|
||||||
|
.nodedc-home-gantt-canvas {
|
||||||
|
min-height: 24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-grid {
|
||||||
|
inset: 1rem 1rem 1rem 9.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-grid-column {
|
||||||
|
padding-left: 0.35rem;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-gantt-row {
|
||||||
|
grid-template-columns: minmax(7rem, 8.25rem) minmax(0, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-height: 3.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-home-card {
|
.nodedc-home-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -1545,11 +1889,8 @@
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
border-radius: 2rem !important;
|
border-radius: 2rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.026) 0%, rgba(255, 255, 255, 0.008) 100%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.022) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(10, 10, 12, 0.68) !important;
|
||||||
rgba(10, 10, 12, 0.58) !important;
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
|
||||||
box-shadow:
|
|
||||||
0 18px 40px rgba(0, 0, 0, 0.2),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
|
|
||||||
-webkit-backdrop-filter: blur(28px);
|
-webkit-backdrop-filter: blur(28px);
|
||||||
backdrop-filter: blur(28px);
|
backdrop-filter: blur(28px);
|
||||||
}
|
}
|
||||||
|
|
@ -1578,35 +1919,175 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-project-card {
|
.nodedc-home-project-card {
|
||||||
height: 14.25rem;
|
height: 15.5rem;
|
||||||
|
position: relative;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 1.75rem !important;
|
border-radius: 1.75rem !important;
|
||||||
box-shadow:
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03) !important;
|
||||||
0 18px 38px rgba(0, 0, 0, 0.22),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.03) !important;
|
|
||||||
transition:
|
transition:
|
||||||
transform 180ms ease,
|
transform 180ms ease,
|
||||||
box-shadow 180ms ease,
|
box-shadow 180ms ease,
|
||||||
filter 180ms ease;
|
filter 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-deck-scroller {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-deck-scroller::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-deck-row {
|
||||||
|
gap: 0;
|
||||||
|
padding-right: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-deck-row > .nodedc-home-project-card + .nodedc-home-project-card {
|
||||||
|
margin-left: -5.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-card-horizontal {
|
||||||
|
width: 16rem;
|
||||||
|
min-width: 16rem;
|
||||||
|
height: 13.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-card::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: inherit;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-card > :last-child {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
transition: opacity 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-home-project-card[data-active="true"] {
|
.nodedc-home-project-card[data-active="true"] {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 28px 48px rgba(0, 0, 0, 0.28),
|
|
||||||
inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.28),
|
inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.28),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.03) !important;
|
inset 0 1px 0 rgba(255, 255, 255, 0.03) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-project-card[data-active="false"] {
|
.nodedc-home-project-card[data-active="false"] {
|
||||||
filter: saturate(0.88);
|
filter: saturate(0.66) brightness(0.62);
|
||||||
transform: scale(0.965);
|
transform: scale(0.965);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-card[data-active="false"]::after {
|
||||||
|
opacity: 1;
|
||||||
|
background: linear-gradient(180deg, rgba(4, 4, 7, 0.14) 0%, rgba(4, 4, 7, 0.42) 100%), rgba(7, 7, 10, 0.28);
|
||||||
|
-webkit-backdrop-filter: blur(14px);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-card[data-active="false"] > :last-child {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-home-project-card[data-active="false"]:hover {
|
.nodedc-home-project-card[data-active="false"]:hover {
|
||||||
transform: translateY(-0.25rem) scale(0.972);
|
transform: translateY(-0.25rem) scale(0.972);
|
||||||
filter: saturate(1);
|
filter: saturate(0.74) brightness(0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-card[data-active="false"]:hover::after {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-project-card[data-active="false"]:hover > :last-child {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-user-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
isolation: isolate;
|
||||||
|
border-radius: 2rem !important;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, rgba(var(--nodedc-accent-rgb), 0.22), transparent 42%),
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(10, 10, 12, 0.82) !important;
|
||||||
|
box-shadow:
|
||||||
|
0 24px 52px rgba(0, 0, 0, 0.24),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.024) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-user-card-orb {
|
||||||
|
position: absolute;
|
||||||
|
top: -4.5rem;
|
||||||
|
right: -3rem;
|
||||||
|
width: 13rem;
|
||||||
|
height: 13rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 32% 32%, rgba(255, 255, 255, 0.34), transparent 32%),
|
||||||
|
radial-gradient(circle at center, rgba(var(--nodedc-accent-rgb), 0.88), rgba(255, 255, 255, 0.04) 72%);
|
||||||
|
opacity: 0.85;
|
||||||
|
filter: blur(12px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-focus-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
isolation: isolate;
|
||||||
|
border-radius: 2rem !important;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(10, 10, 12, 0.72) !important;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-focus-card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, rgba(var(--nodedc-accent-rgb), 0.035), transparent 32%),
|
||||||
|
linear-gradient(135deg, rgba(255, 255, 255, 0.018), transparent 40%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-focus-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 2.25rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.06) !important;
|
||||||
|
padding: 0.5rem 0.9rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-focus-track {
|
||||||
|
height: 0.62rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-focus-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(var(--nodedc-accent-rgb), 0.96) 0%,
|
||||||
|
rgba(var(--nodedc-accent-rgb), 0.58) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-task-deck-scroller {
|
.nodedc-home-task-deck-scroller {
|
||||||
|
|
@ -1620,9 +2101,22 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-home-task-deck-scroller-compact {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-task-deck-row-compact {
|
||||||
|
gap: 0;
|
||||||
|
padding-right: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-task-deck-row-compact > .nodedc-home-task-card + .nodedc-home-task-card {
|
||||||
|
margin-left: -4.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-home-task-card {
|
.nodedc-home-task-card {
|
||||||
width: 18.5rem;
|
width: 17.5rem;
|
||||||
min-width: 18.5rem;
|
min-width: 17.5rem;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
|
|
@ -1636,27 +2130,31 @@
|
||||||
filter 180ms ease;
|
filter 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-home-task-card-compact {
|
||||||
|
width: 10.75rem;
|
||||||
|
min-width: 10.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-home-task-card[data-active="true"] {
|
.nodedc-home-task-card[data-active="true"] {
|
||||||
transform: translateY(-0.85rem) scale(1.015);
|
transform: translateY(-0.35rem);
|
||||||
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-task-card[data-active="false"] {
|
.nodedc-home-task-card[data-active="false"] {
|
||||||
filter: saturate(0.88);
|
filter: saturate(0.7) brightness(0.68);
|
||||||
transform: scale(0.975);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-task-card[data-active="false"]:hover {
|
.nodedc-home-task-card[data-active="false"]:hover {
|
||||||
transform: translateY(-0.2rem) scale(0.985);
|
transform: translateY(-0.18rem);
|
||||||
filter: saturate(1);
|
filter: saturate(0.78) brightness(0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-task-card-surface {
|
.nodedc-home-task-card-surface {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
border-radius: 2rem !important;
|
border-radius: 2rem !important;
|
||||||
box-shadow:
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03) !important;
|
||||||
0 24px 48px rgba(0, 0, 0, 0.24),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.03) !important;
|
|
||||||
-webkit-backdrop-filter: blur(24px);
|
-webkit-backdrop-filter: blur(24px);
|
||||||
backdrop-filter: blur(24px);
|
backdrop-filter: blur(24px);
|
||||||
transition:
|
transition:
|
||||||
|
|
@ -1665,15 +2163,36 @@
|
||||||
color 180ms ease;
|
color 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-home-task-card-surface::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-home-task-card[data-active="false"] .nodedc-home-task-card-surface {
|
.nodedc-home-task-card[data-active="false"] .nodedc-home-task-card-surface {
|
||||||
-webkit-backdrop-filter: blur(24px);
|
-webkit-backdrop-filter: blur(24px);
|
||||||
backdrop-filter: blur(24px);
|
backdrop-filter: blur(24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-home-task-card[data-active="false"] .nodedc-home-task-card-surface::after {
|
||||||
|
opacity: 1;
|
||||||
|
background: linear-gradient(180deg, rgba(3, 3, 5, 0.14) 0%, rgba(3, 3, 5, 0.34) 100%), rgba(7, 7, 10, 0.22);
|
||||||
|
-webkit-backdrop-filter: blur(14px);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodedc-home-task-card[data-active="false"] .nodedc-home-task-card-surface > * {
|
||||||
|
opacity: 0.68;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-home-task-card-surface-passive {
|
.nodedc-home-task-card-surface-passive {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.012) 100%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.035) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(46, 46, 50, 0.9) !important;
|
||||||
rgba(7, 7, 9, 0.74) !important;
|
color: rgba(245, 245, 247, 0.58) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-task-card-surface-active {
|
.nodedc-home-task-card-surface-active {
|
||||||
|
|
@ -1682,7 +2201,6 @@
|
||||||
rgba(var(--nodedc-card-active-rgb), 0.96) !important;
|
rgba(var(--nodedc-card-active-rgb), 0.96) !important;
|
||||||
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 30px 56px rgba(0, 0, 0, 0.28),
|
|
||||||
inset 0 0 0 1px rgba(255, 255, 255, 0.18),
|
inset 0 0 0 1px rgba(255, 255, 255, 0.18),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.04) !important;
|
inset 0 1px 0 rgba(255, 255, 255, 0.04) !important;
|
||||||
}
|
}
|
||||||
|
|
@ -1693,20 +2211,20 @@
|
||||||
-webkit-backdrop-filter: none !important;
|
-webkit-backdrop-filter: none !important;
|
||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 30px 56px rgba(0, 0, 0, 0.28),
|
|
||||||
inset 0 0 0 1px rgba(255, 255, 255, 0.18),
|
inset 0 0 0 1px rgba(255, 255, 255, 0.18),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.04) !important;
|
inset 0 1px 0 rgba(255, 255, 255, 0.04) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nodedc-home-task-card[data-active="true"] .nodedc-home-task-card-surface::after {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-home-task-card-skeleton {
|
.nodedc-home-task-card-skeleton {
|
||||||
height: 14.75rem;
|
height: 14.75rem;
|
||||||
border-radius: 2rem !important;
|
border-radius: 2rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.01) 100%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(7, 7, 9, 0.68) !important;
|
||||||
rgba(7, 7, 9, 0.68) !important;
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
|
||||||
box-shadow:
|
|
||||||
0 24px 48px rgba(0, 0, 0, 0.22),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
|
|
||||||
-webkit-backdrop-filter: blur(24px);
|
-webkit-backdrop-filter: blur(24px);
|
||||||
backdrop-filter: blur(24px);
|
backdrop-filter: blur(24px);
|
||||||
}
|
}
|
||||||
|
|
@ -1714,8 +2232,7 @@
|
||||||
.nodedc-home-metric-card {
|
.nodedc-home-metric-card {
|
||||||
border-radius: 1.5rem !important;
|
border-radius: 1.5rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(7, 7, 9, 0.58);
|
||||||
rgba(7, 7, 9, 0.58);
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 14px 28px rgba(0, 0, 0, 0.14),
|
0 14px 28px rgba(0, 0, 0, 0.14),
|
||||||
|
|
@ -1724,19 +2241,16 @@
|
||||||
|
|
||||||
.nodedc-home-metric-card-accent {
|
.nodedc-home-metric-card-accent {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.022) 0%, rgba(255, 255, 255, 0.008) 100%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.022) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(7, 7, 9, 0.62);
|
||||||
rgba(7, 7, 9, 0.62);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-chart-panel {
|
.nodedc-home-chart-panel {
|
||||||
border-radius: 1.75rem !important;
|
border-radius: 1.75rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%),
|
radial-gradient(circle at top right, rgba(var(--nodedc-accent-rgb), 0.075), transparent 34%),
|
||||||
rgba(7, 7, 9, 0.56);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(0, 0, 0, 0.12);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
box-shadow:
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.016) !important;
|
||||||
0 18px 34px rgba(0, 0, 0, 0.16),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.016) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-progress-track {
|
.nodedc-home-progress-track {
|
||||||
|
|
@ -1755,11 +2269,8 @@
|
||||||
.nodedc-home-subpanel {
|
.nodedc-home-subpanel {
|
||||||
border-radius: 1.5rem !important;
|
border-radius: 1.5rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(0, 0, 0, 0.1) !important;
|
||||||
rgba(6, 6, 8, 0.64) !important;
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.01) !important;
|
||||||
box-shadow:
|
|
||||||
0 14px 28px rgba(0, 0, 0, 0.16),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.01) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-soft-badge {
|
.nodedc-home-soft-badge {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue