diff --git a/plane-src/apps/web/ce/components/home/header.tsx b/plane-src/apps/web/ce/components/home/header.tsx index 09370cf..0f6d205 100644 --- a/plane-src/apps/web/ce/components/home/header.tsx +++ b/plane-src/apps/web/ce/components/home/header.tsx @@ -4,37 +4,105 @@ * See the LICENSE file for details. */ -import { SlidersHorizontal } from "lucide-react"; import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; -import { useHome } from "@/hooks/store/use-home"; -import { useWorkspace } from "@/hooks/store/use-workspace"; +import type { IUser, TActivityEntityData, TProjectAnalyticsCount } from "@plane/types"; +import { useCurrentTime } from "@/hooks/use-current-time"; +import { getActivityProjectId, getCompletionRate, type THomeProjectData } from "@/components/home/home.utils"; -export function HomePageHeader() { - const { t } = useTranslation(); - const { toggleWidgetSettings } = useHome(); - const { currentWorkspace } = useWorkspace(); +type HomePageHeaderProps = { + currentUser?: IUser; + selectedProject?: THomeProjectData; + 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 ( -
-
-
- Workspace Home -
-
- {currentWorkspace?.name ? `Стартовый экран для ${currentWorkspace.name}` : "Главная страница workspace"} -
+
+
+
{heroDateLabel}
+
{timeString}
- -
+
+
+

WORKSPACE HOME

+

+ {selectedProject + ? `${selectedProject.identifier} в фокусе домашней сводки.` + : "Выберите проект для фокуса, Ганта и рабочей аналитики."} +

+
+ +
+
+
Фокус
+
+
+
+ {selectedProject?.name ?? "Workspace"} +
+
+ {selectedProject?.description || selectedProject?.identifier || "Координационный обзор"} +
+
+
+
+ +
+ {marketMetrics.map((metric) => ( +
+
{metric.label}
+
{metric.value}
+
+
+
+
{metric.caption}
+
+ ))} +
+
+
+ ); } diff --git a/plane-src/apps/web/core/components/home/home-dashboard-widgets.tsx b/plane-src/apps/web/core/components/home/home-dashboard-widgets.tsx index 79f3035..f1c6cd1 100644 --- a/plane-src/apps/web/core/components/home/home-dashboard-widgets.tsx +++ b/plane-src/apps/web/core/components/home/home-dashboard-widgets.tsx @@ -7,36 +7,29 @@ import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; -import { useTheme } from "next-themes"; import useSWR from "swr"; // plane imports import { useTranslation } from "@plane/i18n"; import type { IUser, THomeWidgetKeys, THomeWidgetProps, TProjectAnalyticsCount } from "@plane/types"; 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 import { useHome } from "@/hooks/store/use-home"; import { useProject } from "@/hooks/store/use-project"; -import { useWorkspace } from "@/hooks/store/use-workspace"; // plane web components import { HomePageHeader } from "@/plane-web/components/home/header"; import { ProjectService } from "@/services/project"; import { WorkspaceService } from "@/services/workspace.service"; // local imports import { HomeCardShell } from "./home-card-shell"; +import { HomeGanttPreview } from "./home-gantt-preview"; 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 { aggregateProjectAnalytics, type THomeProjectData } from "./home.utils"; import { StickiesWidget } from "../stickies/widget"; import { HomeLoader, NoProjectsEmptyState, RecentActivityWidget } from "./widgets"; import { DashboardQuickLinks } from "./widgets/links"; import { ManageWidgetsModal } from "./widgets/manage"; -import { UserGreetingsView } from "./user-greetings"; const projectService = new ProjectService(); 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(); // navigation const pathname = usePathname(); - // theme hook - const { resolvedTheme } = useTheme(); // store hooks const { toggleWidgetSettings, widgetsMap, showWidgetSettings, loading } = useHome(); const { loader, joinedProjectIds, getPartialProjectById, fetchProjectAnalyticsCount, getProjectAnalyticsCountById } = useProject(); - const { currentWorkspace } = useWorkspace(); // plane hooks - const { t, currentLocale } = useTranslation(); + const { currentLocale } = useTranslation(); // states const [selectedProjectId, setSelectedProjectId] = useState(null); - // derived values - const noWidgetsResolvedPath = resolvedTheme === "light" ? lightWidgetsAsset : darkWidgetsAsset; // derived values 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 isQuickLinksEnabled = !!widgetsMap.quick_links?.is_enabled; const isStickiesEnabled = !!widgetsMap.my_stickies?.is_enabled; - const hasDashboardContent = isRecentsEnabled || isQuickLinksEnabled || isStickiesEnabled; + const hasSecondaryWidgets = isQuickLinksEnabled || isStickiesEnabled; if (!workspaceSlugValue) return null; if (loading || loader !== "loaded") return ; - const recentsCard = isRecentsEnabled ? ( - - - + const recentActivityCard = isRecentsEnabled ? ( + ) : null; const sideWidgetCards = [ @@ -204,78 +191,69 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo return (
- + toggleWidgetSettings(false)} /> -
+
- {currentUser && ( - - )} - - - - + - - {!isWikiApp && } - - {hasDashboardContent ? ( - <> - {recentsCard && sideWidgetCards.length > 0 ? ( -
- {recentsCard} -
{sideWidgetCards}
-
- ) : recentsCard ? ( - recentsCard - ) : ( -
1, - })} - > - {sideWidgetCards} -
- )} - - ) : ( - -
- -
-
- )}
+ + + + + +
+ {!isWikiApp && } + + {hasSecondaryWidgets && ( +
1, + })} + > + {sideWidgetCards} +
+ )} +
); }); diff --git a/plane-src/apps/web/core/components/home/home-gantt-preview.tsx b/plane-src/apps/web/core/components/home/home-gantt-preview.tsx new file mode 100644 index 0000000..012a781 --- /dev/null +++ b/plane-src/apps/web/core/components/home/home-gantt-preview.tsx @@ -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("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 ( +
+
+
+
+ +
+
+
Календарное окно Ганта
+
+ {project ? `${project.name} / ближайший рабочий горизонт` : "Выберите проект для живого окна"} +
+
+
+ +
+ {GANTT_RANGES.map((item) => ( + + ))} + + +
+
+ +
+
+
+ + +
+ {isLoading + ? Array.from({ length: 4 }, (_, index) => ( +
+ )) + : visibleItems.map((item) => ( +
+
+
{item.label}
+
{item.subtitle}
+
+
+
+
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/plane-src/apps/web/core/components/home/home-project-insights.tsx b/plane-src/apps/web/core/components/home/home-project-insights.tsx index 6eaf63f..95811b8 100644 --- a/plane-src/apps/web/core/components/home/home-project-insights.tsx +++ b/plane-src/apps/web/core/components/home/home-project-insights.tsx @@ -4,11 +4,9 @@ * 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 type { TActivityEntityData, TProjectAnalyticsCount } from "@plane/types"; -import { cn } from "@plane/utils"; -import { HomeCardShell } from "./home-card-shell"; import { aggregateProjectAnalytics, getActivityProjectId, @@ -22,6 +20,7 @@ type HomeProjectInsightsProps = { analyticsCollection?: TProjectAnalyticsCount[]; recents?: TActivityEntityData[]; locale: string; + recentActivitySlot?: ReactNode; }; type TActivityPoint = { @@ -33,10 +32,10 @@ type TActivityPoint = { const formatCompactNumber = (value: number) => new Intl.NumberFormat("ru-RU", { notation: "compact" }).format(value); const buildChartPaths = (data: TActivityPoint[]) => { - const width = 420; - const height = 180; - const paddingX = 10; - const paddingY = 18; + const width = 720; + const height = 220; + const paddingX = 24; + const paddingY = 26; const maxValue = Math.max(...data.map((item) => item.value), 1); const stepX = data.length > 1 ? (width - paddingX * 2) / (data.length - 1) : 0; @@ -46,17 +45,28 @@ const buildChartPaths = (data: TActivityPoint[]) => { return { x, y }; }); - const linePath = points - .map((point, index) => `${index === 0 ? "M" : "L"} ${point.x.toFixed(2)} ${point.y.toFixed(2)}`) - .join(" "); + const linePath = points.reduce((path, point, index) => { + if (index === 0) return `M ${point.x.toFixed(2)} ${point.y.toFixed(2)}`; + + 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)} ${( height - paddingY ).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 chartId = useId(); @@ -155,61 +165,58 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) { }, ]; - return ( - -
-
-
- {metricCards.map((metric) => ( -
-
-
{metric.label}
-
{metric.icon}
-
-
{metric.value}
-
{metric.caption}
-
- ))} -
+ return { + activitySeries, + chart, + chartId, + completedIssues, + completionRate, + metricCards, + openIssues, + progressRows, + project, + recentTouchpoints, + totalIssues, + }; +}; -
-
+export function HomeActivityTrendCard(props: HomeProjectInsightsProps) { + const { activitySeries, chart, chartId, project, recentTouchpoints } = useHomeProjectInsightData(props); + + return ( +
+
+
+
+
-
Темп активности
-
Последние 7 дней переходов и взаимодействий внутри сводки.
-
-
- {recentTouchpoints} событий +
+ {project?.identifier ?? "Workspace"} +
+
+ +
Темп активности
+
+
Последние 7 дней переходов и взаимодействий.
+
{recentTouchpoints} событий
-
-
- {["col-1", "col-2", "col-3", "col-4"].map((key) => ( -
+
+
+ {["col-1", "col-2", "col-3", "col-4", "col-5", "col-6"].map((key) => ( +
))}
- + @@ -222,7 +229,7 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) { x2={x} y1={12} 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} y1={y} y2={y} - stroke="rgba(255,255,255,0.05)" + stroke="rgba(255,255,255,0.06)" strokeDasharray="4 6" /> ); @@ -245,30 +252,34 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) { d={chart.linePath} fill="none" stroke="rgb(var(--nodedc-accent-rgb))" - strokeWidth="4" + strokeWidth="5" strokeLinecap="round" + strokeLinejoin="round" /> + + +
{activitySeries.map((activityPoint, index) => { - const point = chart.points[index]; + const point = chart.pointPercents[index]; if (!point) return null; return ( - ); })} - +
-
+
{activitySeries.map((point) => ( -
+
{point.label}
{point.value}
@@ -277,93 +288,146 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
+
+
+ ); +} + +export function HomeOperationsOverview(props: HomeProjectInsightsProps) { + const { recentActivitySlot } = props; + const { + completedIssues, + completionRate, + metricCards, + openIssues, + progressRows, + project, + recentTouchpoints, + totalIssues, + } = useHomeProjectInsightData(props); + + return ( +
+
+
+
+ +
+
+
Операционный срез
+
Команда, циклы и модули относительно текущего workspace.
+
+
-
-
-
- -
-
-
Операционный срез
-
- Нагрузка команды, циклов и модулей относительно остального workspace. + {progressRows.map((row) => { + const percent = row.max > 0 ? Math.max((row.value / row.max) * 100, row.value > 0 ? 10 : 0) : 0; + + return ( +
+
+ {row.label} + {row.value} +
+
+
+ ); + })} +
+
+ +
+
+
+
Ритм исполнения
+
Закрытый объём и открытый остаток по фокусу.
+
+
+ +
+
+ +
+ {metricCards.map((metric) => ( +
+
{metric.label}
+
{metric.value}
+ ))} +
-
- {progressRows.map((row) => { - const percent = row.max > 0 ? Math.max((row.value / row.max) * 100, row.value > 0 ? 10 : 0) : 0; - - return ( -
-
- {row.label} - {row.value} -
-
-
-
-
- ); - })} +
+
+
+ Закрытые задачи + {completedIssues} +
+
+
0 ? (completedIssues / totalIssues) * 100 : 0}%` }} + />
-
-
-
-
Ритм исполнения
-
Сколько уже закрыто и какой объём ещё держим открытым.
-
-
- {completionRate}% -
+
+
+ Открытый остаток + {openIssues}
- -
-
-
- Закрытые задачи - {completedIssues} -
-
-
0 ? (completedIssues / totalIssues) * 100 : 0}%` }} - /> -
-
- -
-
- Открытый остаток - {openIssues} -
-
-
0 ? (openIssues / totalIssues) * 100 : 0}%`, - height: "100%", - }} - /> -
-
- -
- {project ? project.identifier : "Workspace"} - держит - {totalIssues} - задач в общей матрице и - {recentTouchpoints} - недавних касаний за неделю. -
+
+
0 ? (openIssues / totalIssues) * 100 : 0}%`, + height: "100%", + }} + />
+ +
+ {project ? project.identifier : "Workspace"} + держит + {totalIssues} + задач и + {recentTouchpoints} + недавних касаний. +
- + +
+ {recentActivitySlot ? ( +
{recentActivitySlot}
+ ) : ( +
+
+
+
+ +
+
+
Недавние
+
Виджет recent activity отключен в настройках.
+
+
+
+
{completionRate}% готовность
+
+ )} +
+
+ ); +} + +export function HomeProjectInsights(props: HomeProjectInsightsProps) { + return ( +
+ + +
); } diff --git a/plane-src/apps/web/core/components/home/home-project-stack.tsx b/plane-src/apps/web/core/components/home/home-project-stack.tsx index 6b2eda7..b96e3b5 100644 --- a/plane-src/apps/web/core/components/home/home-project-stack.tsx +++ b/plane-src/apps/web/core/components/home/home-project-stack.tsx @@ -4,8 +4,7 @@ * See the LICENSE file for details. */ -import Link from "next/link"; -import { ArrowUpRight, FolderOpenDot, Layers3, UsersRound } from "lucide-react"; +import { FolderOpenDot, Layers3, Search, UsersRound } from "lucide-react"; import type { TActivityEntityData, TProjectAnalyticsCount } from "@plane/types"; import { Logo } from "@plane/propel/emoji-icon-picker"; import { cn } from "@plane/utils"; @@ -14,20 +13,29 @@ import { HomeCardShell } from "./home-card-shell"; import { getActivityProjectId, getCompletionRate, type THomeProjectData } from "./home.utils"; type HomeProjectStackProps = { + className?: string; projects: THomeProjectData[]; analyticsMap: Record; + orientation?: "horizontal" | "vertical"; recents?: TActivityEntityData[]; selectedProjectId: string | null; onSelectProject: (projectId: string) => void; - workspaceSlug: string; }; const STACK_VISIBLE_LIMIT = 4; -const ACTIVE_CARD_HEIGHT = 228; -const STACK_OFFSET = 76; +const ACTIVE_CARD_HEIGHT = 248; +const STACK_OFFSET = 88; 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 orderedProjects = activeProject @@ -45,19 +53,14 @@ export function HomeProjectStack(props: HomeProjectStackProps) { const selectedProject = orderedProjects.find((project: THomeProjectData) => project.id === selectedProjectId) ?? orderedProjects[0]; - const selectedProjectPath = selectedProject ? `/${workspaceSlug}/projects/${selectedProject.id}/issues` : null; const stackHeight = visibleProjects.length > 0 ? ACTIVE_CARD_HEIGHT + (visibleProjects.length - 1) * STACK_OFFSET : 228; + const isHorizontal = orientation === "horizontal"; if (projects.length === 0) { return ( - -
+ +
@@ -74,160 +77,169 @@ export function HomeProjectStack(props: HomeProjectStackProps) { ); } - return ( - - Открыть проект - - - ) : null - } - > -
-
- {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; + const renderProjectCard = (project: THomeProjectData, index: number, horizontal: boolean) => { + 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 ( + + ); + }; + + return ( +
+
+
+
Quick Project
+
Выбор проекта
+
+ +
+ + {isHorizontal ? ( +
+
+ {visibleProjects.map((project: THomeProjectData, index: number) => renderProjectCard(project, index, true))} +
+
+ ) : ( +
+ {visibleProjects.map((project: THomeProjectData, index: number) => renderProjectCard(project, index, false))} +
+ )} + +
+
+
+
Быстрый выбор
+
Все проекты пользователя в текущем workspace.
+
+
+ + {projects.length} +
+
+ +
+ {orderedProjects.map((project: THomeProjectData) => { + const analytics = analyticsMap[project.id]; return ( ); })}
-
-
-
-
Быстрый выбор
-
Все проекты пользователя в текущем workspace.
+ {selectedProject && ( +
+
+
Фокус
+
{selectedProject.identifier}
-
- - {projects.length} +
+
+ + Команда +
+
+ {analyticsMap[selectedProject.id]?.total_members ?? 0} +
+
+
+
Контур
+
+ {activityCountByProject[selectedProject.id] ?? 0} касаний +
- -
- {orderedProjects.map((project: THomeProjectData) => { - const analytics = analyticsMap[project.id]; - return ( - - ); - })} -
- - {selectedProject && ( -
-
-
Фокус
-
{selectedProject.identifier}
-
-
-
- - Команда -
-
- {analyticsMap[selectedProject.id]?.total_members ?? 0} -
-
-
-
Контур
-
- {activityCountByProject[selectedProject.id] ?? 0} recent -
-
-
- )} -
+ )}
- +
); } diff --git a/plane-src/apps/web/styles/globals.css b/plane-src/apps/web/styles/globals.css index 5f9ff03..5df8802 100644 --- a/plane-src/apps/web/styles/globals.css +++ b/plane-src/apps/web/styles/globals.css @@ -1537,6 +1537,350 @@ 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 { position: relative; overflow: hidden; @@ -1545,11 +1889,8 @@ outline: none !important; border-radius: 2rem !important; background: - linear-gradient(180deg, rgba(255, 255, 255, 0.026) 0%, rgba(255, 255, 255, 0.008) 100%), - rgba(10, 10, 12, 0.58) !important; - box-shadow: - 0 18px 40px rgba(0, 0, 0, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.02) !important; + linear-gradient(180deg, rgba(255, 255, 255, 0.022) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(10, 10, 12, 0.68) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02) !important; -webkit-backdrop-filter: blur(28px); backdrop-filter: blur(28px); } @@ -1578,35 +1919,175 @@ } .nodedc-home-project-card { - height: 14.25rem; + height: 15.5rem; + position: relative; border: 0 !important; outline: none !important; overflow: hidden; border-radius: 1.75rem !important; - box-shadow: - 0 18px 38px rgba(0, 0, 0, 0.22), - inset 0 1px 0 rgba(255, 255, 255, 0.03) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03) !important; transition: transform 180ms ease, box-shadow 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"] { 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 1px 0 rgba(255, 255, 255, 0.03) !important; } .nodedc-home-project-card[data-active="false"] { - filter: saturate(0.88); + filter: saturate(0.66) brightness(0.62); 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 { 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 { @@ -1620,9 +2101,22 @@ 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 { - width: 18.5rem; - min-width: 18.5rem; + width: 17.5rem; + min-width: 17.5rem; border: 0 !important; outline: none !important; background: transparent !important; @@ -1636,27 +2130,31 @@ filter 180ms ease; } + .nodedc-home-task-card-compact { + width: 10.75rem; + min-width: 10.75rem; + } + .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"] { - filter: saturate(0.88); - transform: scale(0.975); + filter: saturate(0.7) brightness(0.68); + transform: translateY(0); } .nodedc-home-task-card[data-active="false"]:hover { - transform: translateY(-0.2rem) scale(0.985); - filter: saturate(1); + transform: translateY(-0.18rem); + filter: saturate(0.78) brightness(0.75); } .nodedc-home-task-card-surface { overflow: hidden; isolation: isolate; border-radius: 2rem !important; - box-shadow: - 0 24px 48px rgba(0, 0, 0, 0.24), - inset 0 1px 0 rgba(255, 255, 255, 0.03) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03) !important; -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); transition: @@ -1665,15 +2163,36 @@ 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 { -webkit-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 { background: - linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.012) 100%), - rgba(7, 7, 9, 0.74) !important; + linear-gradient(180deg, rgba(255, 255, 255, 0.035) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(46, 46, 50, 0.9) !important; + color: rgba(245, 245, 247, 0.58) !important; } .nodedc-home-task-card-surface-active { @@ -1682,7 +2201,6 @@ rgba(var(--nodedc-card-active-rgb), 0.96) !important; color: rgb(var(--nodedc-on-card-active-rgb)) !important; box-shadow: - 0 30px 56px rgba(0, 0, 0, 0.28), inset 0 0 0 1px rgba(255, 255, 255, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.04) !important; } @@ -1693,20 +2211,20 @@ -webkit-backdrop-filter: none !important; backdrop-filter: none !important; box-shadow: - 0 30px 56px rgba(0, 0, 0, 0.28), inset 0 0 0 1px rgba(255, 255, 255, 0.18), 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 { height: 14.75rem; border-radius: 2rem !important; background: - linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.01) 100%), - rgba(7, 7, 9, 0.68) !important; - box-shadow: - 0 24px 48px rgba(0, 0, 0, 0.22), - inset 0 1px 0 rgba(255, 255, 255, 0.02) !important; + linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(7, 7, 9, 0.68) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02) !important; -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px); } @@ -1714,8 +2232,7 @@ .nodedc-home-metric-card { border-radius: 1.5rem !important; background: - linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%), - rgba(7, 7, 9, 0.58); + linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(7, 7, 9, 0.58); padding: 1rem; box-shadow: 0 14px 28px rgba(0, 0, 0, 0.14), @@ -1724,19 +2241,16 @@ .nodedc-home-metric-card-accent { background: - linear-gradient(180deg, rgba(255, 255, 255, 0.022) 0%, rgba(255, 255, 255, 0.008) 100%), - rgba(7, 7, 9, 0.62); + linear-gradient(180deg, rgba(255, 255, 255, 0.022) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(7, 7, 9, 0.62); } .nodedc-home-chart-panel { border-radius: 1.75rem !important; background: - linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%), - rgba(7, 7, 9, 0.56); + radial-gradient(circle at top right, rgba(var(--nodedc-accent-rgb), 0.075), transparent 34%), + 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; - box-shadow: - 0 18px 34px rgba(0, 0, 0, 0.16), - inset 0 1px 0 rgba(255, 255, 255, 0.016) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.016) !important; } .nodedc-home-progress-track { @@ -1755,11 +2269,8 @@ .nodedc-home-subpanel { border-radius: 1.5rem !important; background: - linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%), - rgba(6, 6, 8, 0.64) !important; - box-shadow: - 0 14px 28px rgba(0, 0, 0, 0.16), - inset 0 1px 0 rgba(255, 255, 255, 0.01) !important; + linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%), rgba(0, 0, 0, 0.1) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.01) !important; } .nodedc-home-soft-badge {