Compare commits
No commits in common. "4c436a949eab6b02eea138b151c424c96f43b331" and "4d6aba098d43b48722bb915429def22b1af87ef5" have entirely different histories.
4c436a949e
...
4d6aba098d
|
|
@ -28,7 +28,6 @@ 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 { HomeRecentIssueDecks } from "./home-recent-issue-decks";
|
|
||||||
import { HomeProjectInsights } from "./home-project-insights";
|
import { HomeProjectInsights } 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";
|
||||||
|
|
@ -232,8 +231,6 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<HomeRecentIssueDecks project={selectedProject} workspaceSlug={workspaceSlugValue} />
|
|
||||||
|
|
||||||
<HomeProjectInsights
|
<HomeProjectInsights
|
||||||
project={selectedProject}
|
project={selectedProject}
|
||||||
analytics={selectedProjectAnalytics}
|
analytics={selectedProjectAnalytics}
|
||||||
|
|
|
||||||
|
|
@ -190,15 +190,15 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
<div className="text-14 font-semibold text-primary">Темп активности</div>
|
<div className="text-14 font-semibold text-primary">Темп активности</div>
|
||||||
<div className="text-12 text-secondary">Последние 7 дней переходов и взаимодействий внутри сводки.</div>
|
<div className="text-12 text-secondary">Последние 7 дней переходов и взаимодействий внутри сводки.</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="nodedc-home-soft-badge rounded-full px-3 py-1.5 text-12 text-secondary">
|
<div className="rounded-full bg-white/6 px-3 py-1.5 text-12 text-secondary">
|
||||||
{recentTouchpoints} событий
|
{recentTouchpoints} событий
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-home-subpanel relative overflow-hidden p-4">
|
<div className="relative overflow-hidden rounded-[24px] border border-white/6 bg-black/12 p-4">
|
||||||
<div className="absolute inset-x-6 top-4 bottom-4 grid grid-cols-4 gap-4 opacity-25">
|
<div className="absolute inset-x-6 top-4 bottom-4 grid grid-cols-4 gap-4 opacity-25">
|
||||||
{["col-1", "col-2", "col-3", "col-4"].map((key) => (
|
{["col-1", "col-2", "col-3", "col-4"].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/8 last:border-r-0" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -268,7 +268,7 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
|
|
||||||
<div className="relative z-[1] mt-4 grid grid-cols-7 gap-2">
|
<div className="relative z-[1] 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="rounded-2xl bg-white/4 px-2 py-2 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>
|
||||||
|
|
@ -279,9 +279,9 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="nodedc-home-chart-panel p-5">
|
<div className="rounded-[28px] border border-white/6 bg-[rgba(var(--nodedc-accent-rgb),0.08)] p-5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="grid size-11 place-items-center rounded-2xl bg-[rgba(0,0,0,0.28)] text-[rgb(var(--nodedc-accent-rgb))]">
|
<div className="grid size-11 place-items-center rounded-2xl bg-[rgba(var(--nodedc-accent-rgb),0.18)] text-[rgb(var(--nodedc-accent-rgb))]">
|
||||||
<UsersRound className="size-5" />
|
<UsersRound className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -311,19 +311,17 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-home-chart-panel p-5">
|
<div className="rounded-[28px] border border-white/6 bg-black/12 p-5">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-13 font-semibold text-primary">Ритм исполнения</div>
|
<div className="text-13 font-semibold text-primary">Ритм исполнения</div>
|
||||||
<div className="text-12 text-secondary">Сколько уже закрыто и какой объём ещё держим открытым.</div>
|
<div className="text-12 text-secondary">Сколько уже закрыто и какой объём ещё держим открытым.</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="nodedc-home-soft-badge rounded-full px-3 py-1.5 text-12 text-secondary">
|
<div className="rounded-full bg-white/6 px-3 py-1.5 text-12 text-secondary">{completionRate}%</div>
|
||||||
{completionRate}%
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 space-y-4">
|
<div className="mt-5 space-y-4">
|
||||||
<div className="nodedc-home-subpanel p-4">
|
<div className="rounded-[22px] bg-white/4 p-4">
|
||||||
<div className="flex items-center justify-between gap-3 text-12">
|
<div className="flex items-center justify-between gap-3 text-12">
|
||||||
<span className="text-secondary">Закрытые задачи</span>
|
<span className="text-secondary">Закрытые задачи</span>
|
||||||
<span className="font-semibold text-primary">{completedIssues}</span>
|
<span className="font-semibold text-primary">{completedIssues}</span>
|
||||||
|
|
@ -336,7 +334,7 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-home-subpanel p-4">
|
<div className="rounded-[22px] bg-white/4 p-4">
|
||||||
<div className="flex items-center justify-between gap-3 text-12">
|
<div className="flex items-center justify-between gap-3 text-12">
|
||||||
<span className="text-secondary">Открытый остаток</span>
|
<span className="text-secondary">Открытый остаток</span>
|
||||||
<span className="font-semibold text-primary">{openIssues}</span>
|
<span className="font-semibold text-primary">{openIssues}</span>
|
||||||
|
|
@ -352,7 +350,7 @@ export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-home-subpanel p-4 text-12 text-secondary">
|
<div className="rounded-[22px] border border-dashed border-white/8 bg-white/3 p-4 text-12 text-secondary">
|
||||||
<span className="font-semibold text-primary">{project ? project.identifier : "Workspace"}</span>
|
<span className="font-semibold text-primary">{project ? project.identifier : "Workspace"}</span>
|
||||||
<span> держит </span>
|
<span> держит </span>
|
||||||
<span className="font-semibold text-primary">{totalIssues}</span>
|
<span className="font-semibold text-primary">{totalIssues}</span>
|
||||||
|
|
|
||||||
|
|
@ -1,454 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* See the LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { CalendarDays, Sparkles } from "lucide-react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
|
||||||
import { PriorityIcon, StateGroupIcon } from "@plane/propel/icons";
|
|
||||||
import type { TExternalContourRequest, TIssue } from "@plane/types";
|
|
||||||
import { Avatar } from "@plane/ui";
|
|
||||||
import { cn, getFileURL, renderFormattedDate } from "@plane/utils";
|
|
||||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
|
||||||
import { useMember } from "@/hooks/store/use-member";
|
|
||||||
import { useProject } from "@/hooks/store/use-project";
|
|
||||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
|
||||||
import {
|
|
||||||
NodedcWorkItemCard,
|
|
||||||
getNodedcWorkItemCardAppearance,
|
|
||||||
} from "@/components/issues/issue-layouts/shared/nodedc-work-item-card";
|
|
||||||
import { ExternalContourService } from "@/services/external-contours";
|
|
||||||
import { IssueService } from "@/services/issue";
|
|
||||||
import { HomeCardShell } from "./home-card-shell";
|
|
||||||
import type { THomeProjectData } from "./home.utils";
|
|
||||||
|
|
||||||
const issueService = new IssueService();
|
|
||||||
const externalContourService = new ExternalContourService();
|
|
||||||
const INTERNAL_DECK_LIMIT = 10;
|
|
||||||
const EXTERNAL_DECK_LIMIT = 10;
|
|
||||||
const INTERNAL_CURSOR = `${INTERNAL_DECK_LIMIT}:0:0`;
|
|
||||||
|
|
||||||
type HomeRecentIssueDecksProps = {
|
|
||||||
project?: THomeProjectData;
|
|
||||||
workspaceSlug: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DeckSectionProps = {
|
|
||||||
count: number;
|
|
||||||
description: string;
|
|
||||||
emptyDescription: string;
|
|
||||||
emptyTitle: string;
|
|
||||||
isLoading: boolean;
|
|
||||||
items: ReactNode[];
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type InternalIssueCardProps = {
|
|
||||||
isActive: boolean;
|
|
||||||
issue: TIssue;
|
|
||||||
onSelect: () => void;
|
|
||||||
project: THomeProjectData;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ExternalIssueCardProps = {
|
|
||||||
isActive: boolean;
|
|
||||||
onSelect: () => void;
|
|
||||||
project: THomeProjectData;
|
|
||||||
request: TExternalContourRequest;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortByRecentCreatedDate = <
|
|
||||||
T extends { created_at?: string | null; requested_at?: string | null; updated_at?: string | null },
|
|
||||||
>(
|
|
||||||
items: T[]
|
|
||||||
) => {
|
|
||||||
// oxlint-disable-next-line unicorn/no-array-sort
|
|
||||||
return [...items].sort((left: T, right: T) => {
|
|
||||||
const leftDate = Date.parse(left.requested_at ?? left.created_at ?? left.updated_at ?? "") || 0;
|
|
||||||
const rightDate = Date.parse(right.requested_at ?? right.created_at ?? right.updated_at ?? "") || 0;
|
|
||||||
return rightDate - leftDate;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const DeckSection = (props: DeckSectionProps) => {
|
|
||||||
const { count, description, emptyDescription, emptyTitle, isLoading, items, title } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<div className="text-14 font-semibold text-primary">{title}</div>
|
|
||||||
<div className="text-12 text-secondary">{description}</div>
|
|
||||||
</div>
|
|
||||||
<div className="nodedc-toolbar-pill inline-flex items-center gap-2">
|
|
||||||
<Sparkles className="size-3.5" />
|
|
||||||
<span>{count}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="nodedc-home-task-deck-scroller">
|
|
||||||
<div className="flex min-h-[236px] items-end px-1 py-4">
|
|
||||||
{isLoading
|
|
||||||
? Array.from({ length: 4 }, (_, index) => (
|
|
||||||
<div
|
|
||||||
key={`skeleton-${title}-${index}`}
|
|
||||||
className={cn("nodedc-home-task-card nodedc-home-task-card-skeleton animate-pulse", {
|
|
||||||
"-ml-16": index > 0,
|
|
||||||
})}
|
|
||||||
style={{ zIndex: 5 - index }}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
: items}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isLoading && items.length === 0 && (
|
|
||||||
<div className="rounded-[24px] border border-white/6 bg-black/10 px-4 py-5">
|
|
||||||
<div className="text-14 font-semibold text-primary">{emptyTitle}</div>
|
|
||||||
<div className="mt-1 text-12 leading-5 text-secondary">{emptyDescription}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const HomeInternalContourDeckCard = observer(function HomeInternalContourDeckCard(props: InternalIssueCardProps) {
|
|
||||||
const { isActive, issue, onSelect, project } = props;
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { getProjectById } = useProject();
|
|
||||||
const { getUserDetails } = useMember();
|
|
||||||
const { getStateById } = useProjectState();
|
|
||||||
|
|
||||||
const creatorDetails = useMemo(() => {
|
|
||||||
if (issue.created_by_detail) return issue.created_by_detail;
|
|
||||||
if (issue.created_by && getUserDetails(issue.created_by)) return getUserDetails(issue.created_by);
|
|
||||||
if (issue.created_by_display_name || issue.created_by_avatar_url) {
|
|
||||||
return {
|
|
||||||
id: issue.created_by,
|
|
||||||
display_name: issue.created_by_display_name ?? t("common.none"),
|
|
||||||
avatar_url: issue.created_by_avatar_url ?? "",
|
|
||||||
first_name: "",
|
|
||||||
last_name: "",
|
|
||||||
is_bot: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}, [
|
|
||||||
getUserDetails,
|
|
||||||
issue.created_by,
|
|
||||||
issue.created_by_avatar_url,
|
|
||||||
issue.created_by_detail,
|
|
||||||
issue.created_by_display_name,
|
|
||||||
t,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const sourceContourName = issue.source_project_name ?? getProjectById(issue.project_id)?.name ?? project.name;
|
|
||||||
const selectedState = getStateById(issue.state_id);
|
|
||||||
const { iconBubbleClasses, pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
|
|
||||||
const statusIconColor =
|
|
||||||
selectedState?.color ?? (isActive ? "rgb(var(--nodedc-on-card-active-rgb))" : "var(--text-color-primary)");
|
|
||||||
const creatorName = creatorDetails?.display_name ?? t("common.none");
|
|
||||||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
|
||||||
|
|
||||||
const header = (
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
||||||
<div className="shrink-0">
|
|
||||||
<Avatar src={getFileURL(creatorDetails?.avatar_url ?? "")} name={creatorName} size="md" />
|
|
||||||
</div>
|
|
||||||
<div className="truncate text-body-sm-medium leading-5">{creatorName}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
|
||||||
<div className={cn("flex h-8 w-8 items-center justify-center rounded-full", iconBubbleClasses)}>
|
|
||||||
<PriorityIcon priority={issue.priority} className="h-3.5 w-3.5" />
|
|
||||||
</div>
|
|
||||||
<div className={cn("flex h-8 w-8 items-center justify-center rounded-full", iconBubbleClasses)}>
|
|
||||||
<StateGroupIcon
|
|
||||||
stateGroup={selectedState?.group ?? "backlog"}
|
|
||||||
color={statusIconColor}
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
percentage={selectedState?.order}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const footer = (
|
|
||||||
<>
|
|
||||||
<div className={cn("inline-flex min-h-9 items-center rounded-full pl-1 pr-2", pillBackgroundClasses)}>
|
|
||||||
{(issue.assignee_ids?.length ?? 0) > 0 ? (
|
|
||||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
|
||||||
) : (
|
|
||||||
<span className="px-2 text-[11px] font-medium">{t("external_contours_page.list.unassigned")}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cn("inline-flex min-h-9 items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium", pillBackgroundClasses)}>
|
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
|
||||||
<span className="truncate">{dueDateLabel}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button type="button" className="nodedc-home-task-card" data-active={isActive} onClick={onSelect} title={issue.name}>
|
|
||||||
<NodedcWorkItemCard
|
|
||||||
isActive={isActive}
|
|
||||||
surfaceClassName={cn(
|
|
||||||
"nodedc-home-task-card-surface px-0",
|
|
||||||
isActive ? "nodedc-home-task-card-surface-active" : "nodedc-home-task-card-surface-passive"
|
|
||||||
)}
|
|
||||||
contentClassName="px-1"
|
|
||||||
header={header}
|
|
||||||
subtitle={sourceContourName}
|
|
||||||
title={issue.name}
|
|
||||||
footer={footer}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const HomeExternalContourDeckCard = observer(function HomeExternalContourDeckCard(props: ExternalIssueCardProps) {
|
|
||||||
const { isActive, onSelect, project, request } = props;
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { getStateById } = useProjectState();
|
|
||||||
const { iconBubbleClasses, pillBackgroundClasses } = getNodedcWorkItemCardAppearance(isActive);
|
|
||||||
|
|
||||||
const issue = request.issue;
|
|
||||||
const isOutgoing = request.direction
|
|
||||||
? request.direction === "outgoing"
|
|
||||||
: request.source_project_id === project.id;
|
|
||||||
const requester =
|
|
||||||
request.requested_by?.display_name ||
|
|
||||||
request.requested_by_name ||
|
|
||||||
issue.created_by_detail?.display_name ||
|
|
||||||
t("external_contours_page.mirror.system_actor");
|
|
||||||
const requesterAvatar = issue.created_by_detail?.avatar_url || "";
|
|
||||||
const counterpartContourName = isOutgoing
|
|
||||||
? request.target_project?.name || request.target_project_name || issue.project_detail?.name || t("common.none")
|
|
||||||
: request.source_project?.name || request.source_project_name || t("common.none");
|
|
||||||
const fallbackState = getStateById(issue.state_id);
|
|
||||||
const selectedState = issue.state_detail ?? fallbackState;
|
|
||||||
const statusIconColor =
|
|
||||||
selectedState?.color ?? (isActive ? "rgb(var(--nodedc-on-card-active-rgb))" : "var(--text-color-primary)");
|
|
||||||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button type="button" className="nodedc-home-task-card" data-active={isActive} onClick={onSelect} title={issue.name}>
|
|
||||||
<div
|
|
||||||
data-active={isActive}
|
|
||||||
className={cn(
|
|
||||||
"nodedc-external-card nodedc-home-task-card-surface relative flex min-h-[220px] w-full flex-col p-4",
|
|
||||||
isActive ? "nodedc-home-task-card-surface-active" : "nodedc-home-task-card-surface-passive"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={cn("relative flex min-h-[220px] flex-col px-1", isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white")}>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
||||||
<div className="shrink-0">
|
|
||||||
<Avatar src={getFileURL(requesterAvatar)} name={requester} size="md" />
|
|
||||||
</div>
|
|
||||||
<div className="truncate text-body-sm-medium leading-5">{requester}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
|
||||||
{request.has_unread_updates && (
|
|
||||||
<span
|
|
||||||
className={cn("size-2 rounded-full", isActive ? "bg-black/70" : "bg-accent-primary")}
|
|
||||||
title={t("external_contours_page.list.unread_updates")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className={cn("flex h-8 w-8 items-center justify-center rounded-full", iconBubbleClasses)}>
|
|
||||||
<PriorityIcon priority={issue.priority} className="h-3.5 w-3.5" />
|
|
||||||
</div>
|
|
||||||
<div className={cn("flex h-8 w-8 items-center justify-center rounded-full", iconBubbleClasses)}>
|
|
||||||
<StateGroupIcon
|
|
||||||
stateGroup={selectedState?.group ?? "backlog"}
|
|
||||||
color={statusIconColor}
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
percentage={fallbackState?.order}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"truncate -mt-0.5 pl-8 text-[11px] font-medium leading-4",
|
|
||||||
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/72" : "text-[#B3B3B8]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{counterpartContourName}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-1 items-center justify-center px-5 py-4 text-center">
|
|
||||||
<div className="line-clamp-4 max-w-full text-lg font-semibold leading-6">{issue.name}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div className={cn("inline-flex min-h-9 items-center rounded-full pl-1 pr-2", pillBackgroundClasses)}>
|
|
||||||
{(issue.assignee_ids?.length ?? 0) > 0 ? (
|
|
||||||
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids ?? []} size="sm" />
|
|
||||||
) : (
|
|
||||||
<span className="px-2 text-[11px] font-medium">{t("external_contours_page.list.unassigned")}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cn("inline-flex min-h-9 items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium", pillBackgroundClasses)}>
|
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
|
||||||
<span className="truncate">{dueDateLabel}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const HomeRecentIssueDecks = observer(function HomeRecentIssueDecks(props: HomeRecentIssueDecksProps) {
|
|
||||||
const { project, workspaceSlug } = props;
|
|
||||||
const [selectedInternalIssueId, setSelectedInternalIssueId] = useState<string | null>(null);
|
|
||||||
const [selectedExternalRequestId, setSelectedExternalRequestId] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const { data: internalIssueResponse, isLoading: isInternalIssuesLoading } = useSWR(
|
|
||||||
project ? `HOME_PROJECT_INTERNAL_ISSUES_${workspaceSlug}_${project.id}` : null,
|
|
||||||
project
|
|
||||||
? () =>
|
|
||||||
issueService.getIssues(workspaceSlug, project.id, {
|
|
||||||
order_by: "-created_at",
|
|
||||||
per_page: INTERNAL_DECK_LIMIT.toString(),
|
|
||||||
cursor: INTERNAL_CURSOR,
|
|
||||||
})
|
|
||||||
: null,
|
|
||||||
{
|
|
||||||
revalidateIfStale: false,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: externalRequestsResponse, isLoading: isExternalRequestsLoading } = useSWR(
|
|
||||||
project ? `HOME_PROJECT_EXTERNAL_CONTOURS_${workspaceSlug}_${project.id}` : null,
|
|
||||||
project ? () => externalContourService.list(workspaceSlug, project.id) : null,
|
|
||||||
{
|
|
||||||
revalidateIfStale: false,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const internalIssues = useMemo<TIssue[]>(() => {
|
|
||||||
const results = internalIssueResponse?.results;
|
|
||||||
return Array.isArray(results) ? results.slice(0, INTERNAL_DECK_LIMIT) : [];
|
|
||||||
}, [internalIssueResponse]);
|
|
||||||
|
|
||||||
const externalRequests = useMemo<TExternalContourRequest[]>(
|
|
||||||
() => sortByRecentCreatedDate(externalRequestsResponse?.results ?? []).slice(0, EXTERNAL_DECK_LIMIT),
|
|
||||||
[externalRequestsResponse]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (internalIssues.length === 0) {
|
|
||||||
if (selectedInternalIssueId !== null) setSelectedInternalIssueId(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedInternalIssueId || !internalIssues.some((issue) => issue.id === selectedInternalIssueId)) {
|
|
||||||
setSelectedInternalIssueId(internalIssues[0].id);
|
|
||||||
}
|
|
||||||
}, [internalIssues, selectedInternalIssueId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (externalRequests.length === 0) {
|
|
||||||
if (selectedExternalRequestId !== null) setSelectedExternalRequestId(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedExternalRequestId || !externalRequests.some((request) => request.id === selectedExternalRequestId)) {
|
|
||||||
setSelectedExternalRequestId(externalRequests[0].id);
|
|
||||||
}
|
|
||||||
}, [externalRequests, selectedExternalRequestId]);
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
return (
|
|
||||||
<HomeCardShell
|
|
||||||
eyebrow="Task Decks"
|
|
||||||
title="Последние задачи по проекту"
|
|
||||||
description="Выберите проект слева, и здесь появятся колоды последних задач внешнего и внутреннего контуров."
|
|
||||||
>
|
|
||||||
<div className="rounded-[24px] border border-white/6 bg-black/10 px-5 py-6 text-13 text-secondary">
|
|
||||||
Фокус проекта пока не выбран.
|
|
||||||
</div>
|
|
||||||
</HomeCardShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const internalIssueCards = internalIssues.map((issue, index) => (
|
|
||||||
<div
|
|
||||||
key={issue.id}
|
|
||||||
className={cn({ "-ml-16": index > 0 })}
|
|
||||||
style={{ zIndex: issue.id === selectedInternalIssueId ? internalIssues.length + 6 : index + 1 }}
|
|
||||||
>
|
|
||||||
<HomeInternalContourDeckCard
|
|
||||||
issue={issue}
|
|
||||||
isActive={issue.id === selectedInternalIssueId}
|
|
||||||
onSelect={() => setSelectedInternalIssueId(issue.id)}
|
|
||||||
project={project}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
const externalIssueCards = externalRequests.map((request, index) => (
|
|
||||||
<div
|
|
||||||
key={request.id}
|
|
||||||
className={cn({ "-ml-16": index > 0 })}
|
|
||||||
style={{ zIndex: request.id === selectedExternalRequestId ? externalRequests.length + 6 : index + 1 }}
|
|
||||||
>
|
|
||||||
<HomeExternalContourDeckCard
|
|
||||||
isActive={request.id === selectedExternalRequestId}
|
|
||||||
onSelect={() => setSelectedExternalRequestId(request.id)}
|
|
||||||
project={project}
|
|
||||||
request={request}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HomeCardShell
|
|
||||||
eyebrow={`${project.identifier} • последние задачи`}
|
|
||||||
title="Последние задачи проекта"
|
|
||||||
description="Ниже собраны две колоды для быстрого просмотра новых карточек во внешнем и внутреннем контурах."
|
|
||||||
contentClassName="space-y-5 p-5"
|
|
||||||
>
|
|
||||||
<DeckSection
|
|
||||||
count={externalRequests.length}
|
|
||||||
description="Последние запросы и задачи внешнего контура по текущему проекту."
|
|
||||||
emptyDescription="У проекта пока нет внешних контурных задач. Когда появится первый обмен с контрагентом, он сразу попадет в эту колоду."
|
|
||||||
emptyTitle="Внешний контур пока пуст"
|
|
||||||
isLoading={isExternalRequestsLoading}
|
|
||||||
items={externalIssueCards}
|
|
||||||
title="Последние задачи внешнего контура"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeckSection
|
|
||||||
count={internalIssues.length}
|
|
||||||
description="Последние добавленные внутренние задачи выбранного проекта."
|
|
||||||
emptyDescription="Во внутреннем контуре пока нет задач. Как только в проекте появится новая карточка, она ляжет сюда первой."
|
|
||||||
emptyTitle="Внутренний контур пока пуст"
|
|
||||||
isLoading={isInternalIssuesLoading}
|
|
||||||
items={internalIssueCards}
|
|
||||||
title="Последние задачи внутреннего контура"
|
|
||||||
/>
|
|
||||||
</HomeCardShell>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -7,7 +7,10 @@
|
||||||
// plane types
|
// plane types
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import type { IUser, TProjectAnalyticsCount } from "@plane/types";
|
import type { IUser, TProjectAnalyticsCount } from "@plane/types";
|
||||||
|
// plane ui
|
||||||
|
// hooks
|
||||||
import { useCurrentTime } from "@/hooks/use-current-time";
|
import { useCurrentTime } from "@/hooks/use-current-time";
|
||||||
|
import { HomeCardShell } from "./home-card-shell";
|
||||||
import { getCompletionRate, type THomeProjectData } from "./home.utils";
|
import { getCompletionRate, type THomeProjectData } from "./home.utils";
|
||||||
|
|
||||||
export interface IUserGreetingsView {
|
export interface IUserGreetingsView {
|
||||||
|
|
@ -49,38 +52,49 @@ export function UserGreetingsView(props: IUserGreetingsView) {
|
||||||
const completionRate = getCompletionRate(selectedProjectAnalytics);
|
const completionRate = getCompletionRate(selectedProjectAnalytics);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="nodedc-home-card px-5 py-4">
|
<HomeCardShell
|
||||||
<div className="grid items-stretch gap-3 xl:grid-cols-[minmax(0,1fr)_280px_220px]">
|
tone="accent"
|
||||||
<div className="flex min-w-0 items-center">
|
eyebrow={workspaceName ?? "Workspace Home"}
|
||||||
<div className="min-w-0">
|
title={`${t("good")} ${t(greeting)}, ${user?.first_name} ${user?.last_name}`}
|
||||||
<div className="text-[11px] font-semibold tracking-[0.22em] text-placeholder uppercase">
|
description={`${weekDay}, ${date} ${timeString}`}
|
||||||
{workspaceName ?? "Workspace Home"}
|
contentClassName="p-5"
|
||||||
|
>
|
||||||
|
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||||
|
<div className="rounded-[28px] border border-white/6 bg-black/10 p-4">
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-full bg-white/8 px-3 py-1.5 text-12 text-secondary">
|
||||||
|
<span>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</span>
|
||||||
|
<span>Главная панель workspace</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 max-w-2xl text-13 leading-6 text-secondary">
|
||||||
|
Домашняя страница теперь собирает проектный фокус, recent activity, быстрые ссылки и стикеры в один рабочий
|
||||||
|
экран без переходов по разделам.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-1">
|
||||||
|
<div className="rounded-[24px] border border-white/6 bg-black/10 p-4">
|
||||||
|
<div className="text-12 font-medium text-secondary">Текущий фокус</div>
|
||||||
|
<div className="mt-2 text-16 font-semibold text-primary">
|
||||||
|
{selectedProject ? selectedProject.name : "Выберите проект слева"}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-12 text-secondary">
|
||||||
|
{selectedProject ? selectedProject.identifier : "Домашняя сводка перестроится под выбранную карточку."}
|
||||||
</div>
|
</div>
|
||||||
<h2 className="mt-1 truncate text-24 font-semibold text-primary">
|
|
||||||
{`${t("good")} ${t(greeting)}, ${user?.first_name} ${user?.last_name}`}
|
|
||||||
</h2>
|
|
||||||
<div className="mt-1 text-13 text-secondary">{`${weekDay}, ${date} ${timeString}`}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="nodedc-home-subpanel flex min-h-[108px] flex-col justify-center px-4 py-3">
|
<div className="rounded-[24px] border border-white/6 bg-black/10 p-4">
|
||||||
<div className="text-12 font-medium text-secondary">Текущий фокус</div>
|
<div className="text-12 font-medium text-secondary">Прогресс фокуса</div>
|
||||||
<div className="mt-2 line-clamp-2 text-18 font-semibold text-primary">
|
<div className="mt-2 text-16 font-semibold text-primary">
|
||||||
{selectedProject ? selectedProject.name : "Выберите проект слева"}
|
{selectedProject ? `${completionRate}%` : "—"}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-12 text-secondary">
|
<div className="mt-1 text-12 text-secondary">
|
||||||
{selectedProject ? selectedProject.identifier : "Домашняя сводка перестроится под выбранную карточку."}
|
{selectedProject
|
||||||
</div>
|
? "Закрытые задачи относительно общего объёма."
|
||||||
</div>
|
: "Станет доступен после выбора проекта."}
|
||||||
|
</div>
|
||||||
<div className="nodedc-home-subpanel flex min-h-[108px] flex-col justify-center px-4 py-3">
|
|
||||||
<div className="text-12 font-medium text-secondary">Прогресс фокуса</div>
|
|
||||||
<div className="mt-2 text-18 font-semibold text-primary">{selectedProject ? `${completionRate}%` : "—"}</div>
|
|
||||||
<div className="mt-1 text-12 text-secondary">
|
|
||||||
{selectedProject ? "Закрытые задачи относительно общего объёма." : "Станет доступен после выбора проекта."}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</HomeCardShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1545,11 +1545,11 @@
|
||||||
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.034) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
rgba(10, 10, 12, 0.58) !important;
|
rgba(255, 255, 255, 0.028) !important;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 18px 40px rgba(0, 0, 0, 0.2),
|
0 18px 40px rgba(0, 0, 0, 0.18),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
|
inset 0 1px 0 rgba(255, 255, 255, 0.028) !important;
|
||||||
-webkit-backdrop-filter: blur(28px);
|
-webkit-backdrop-filter: blur(28px);
|
||||||
backdrop-filter: blur(28px);
|
backdrop-filter: blur(28px);
|
||||||
}
|
}
|
||||||
|
|
@ -1560,15 +1560,15 @@
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top right, rgba(var(--nodedc-accent-rgb), 0.08), transparent 34%),
|
radial-gradient(circle at top right, rgba(var(--nodedc-accent-rgb), 0.12), transparent 34%),
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.01) 0%, transparent 100%);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.014) 0%, transparent 100%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-card[data-tone="accent"] {
|
.nodedc-home-card[data-tone="accent"] {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.042) 0%, rgba(255, 255, 255, 0.016) 100%),
|
||||||
rgba(var(--nodedc-accent-rgb), 0.1) !important;
|
rgba(var(--nodedc-accent-rgb), 0.12) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-card[data-tone="accent"]::before {
|
.nodedc-home-card[data-tone="accent"]::before {
|
||||||
|
|
@ -1609,141 +1609,33 @@
|
||||||
filter: saturate(1);
|
filter: saturate(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-task-deck-scroller {
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: visible;
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-task-deck-scroller::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-task-card {
|
|
||||||
width: 18.5rem;
|
|
||||||
min-width: 18.5rem;
|
|
||||||
border: 0 !important;
|
|
||||||
outline: none !important;
|
|
||||||
background: transparent !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
transition:
|
|
||||||
transform 180ms ease,
|
|
||||||
filter 180ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-task-card[data-active="true"] {
|
|
||||||
transform: translateY(-0.85rem) scale(1.015);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-task-card[data-active="false"] {
|
|
||||||
filter: saturate(0.88);
|
|
||||||
transform: scale(0.975);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-task-card[data-active="false"]:hover {
|
|
||||||
transform: translateY(-0.2rem) scale(0.985);
|
|
||||||
filter: saturate(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
-webkit-backdrop-filter: blur(24px);
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
transition:
|
|
||||||
background 180ms ease,
|
|
||||||
box-shadow 180ms ease,
|
|
||||||
color 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-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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-task-card-surface-active {
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.018) 100%),
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-task-card[data-active="true"] .nodedc-home-task-card-surface {
|
|
||||||
background: rgb(var(--nodedc-card-active-rgb)) !important;
|
|
||||||
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
|
||||||
-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-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;
|
|
||||||
-webkit-backdrop-filter: blur(24px);
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-metric-card {
|
.nodedc-home-metric-card {
|
||||||
border-radius: 1.5rem !important;
|
border-radius: 1.5rem !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
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.032) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(0, 0, 0, 0.14);
|
||||||
rgba(7, 7, 9, 0.58);
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
box-shadow:
|
|
||||||
0 14px 28px rgba(0, 0, 0, 0.14),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.016) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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.042) 0%, rgba(255, 255, 255, 0.016) 100%),
|
||||||
rgba(7, 7, 9, 0.62);
|
rgba(var(--nodedc-accent-rgb), 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-chart-panel {
|
.nodedc-home-chart-panel {
|
||||||
border-radius: 1.75rem !important;
|
border-radius: 1.75rem !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
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.032) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(0, 0, 0, 0.14);
|
||||||
rgba(7, 7, 9, 0.56);
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
box-shadow:
|
|
||||||
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 {
|
||||||
height: 0.55rem;
|
height: 0.55rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-progress-fill {
|
.nodedc-home-progress-fill {
|
||||||
|
|
@ -1752,21 +1644,6 @@
|
||||||
background: linear-gradient(90deg, rgba(var(--nodedc-accent-rgb), 0.94) 0%, rgba(255, 255, 255, 0.92) 100%);
|
background: linear-gradient(90deg, rgba(var(--nodedc-accent-rgb), 0.94) 0%, rgba(255, 255, 255, 0.92) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-soft-badge {
|
|
||||||
background: rgba(0, 0, 0, 0.34) !important;
|
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.01) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-workspace-list-row:hover {
|
.nodedc-workspace-list-row:hover {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.044) 0%, rgba(255, 255, 255, 0.018) 100%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.044) 0%, rgba(255, 255, 255, 0.018) 100%),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue