Compare commits
No commits in common. "c18fa2b7e9ce9145b739725192ba750ac0d4462c" and "c8a669e01ccbd732099798afee5242c8f3404391" have entirely different histories.
c18fa2b7e9
...
c8a669e01c
|
After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 456 KiB After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 525 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 216 KiB |
|
|
@ -4,37 +4,6 @@
|
||||||
* See the LICENSE file for details.
|
* 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";
|
|
||||||
|
|
||||||
export function HomePageHeader() {
|
export function HomePageHeader() {
|
||||||
const { t } = useTranslation();
|
return <></>;
|
||||||
const { toggleWidgetSettings } = useHome();
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
||||||
<div className="flex min-w-0 flex-col gap-1">
|
|
||||||
<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">
|
|
||||||
<span>Workspace Home</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-13 text-secondary">
|
|
||||||
{currentWorkspace?.name ? `Стартовый экран для ${currentWorkspace.name}` : "Главная страница workspace"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
className="nodedc-toolbar-pill"
|
|
||||||
prependIcon={<SlidersHorizontal className="size-4" />}
|
|
||||||
onClick={() => toggleWidgetSettings(true)}
|
|
||||||
>
|
|
||||||
{t("home.manage_widgets")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -215,23 +215,23 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Popover.Panel
|
<Popover.Panel
|
||||||
className="nodedc-glass-modal nodedc-glass-popup-surface absolute right-0 z-20 mt-3 overflow-hidden rounded-[1.75rem]"
|
className="absolute right-0 z-20 mt-2 rounded-md border border-subtle bg-surface-1 shadow-raised-200"
|
||||||
static
|
static
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={imagePickerRef}
|
ref={imagePickerRef}
|
||||||
className="flex h-[32rem] w-[21rem] flex-col overflow-hidden rounded-[1.75rem] md:h-[38rem] md:w-[38rem]"
|
className="flex h-96 w-80 flex-col overflow-auto rounded border border-subtle bg-surface-1 shadow-raised-200 md:h-[36rem] md:w-[36rem]"
|
||||||
>
|
>
|
||||||
<Tabs defaultValue={enabledTabs[0]?.key || "images"} className="flex h-full flex-col px-4 pt-4 pb-3">
|
<Tabs defaultValue={enabledTabs[0]?.key || "images"} className="flex h-full flex-col p-3">
|
||||||
<Tabs.List className="rounded-[1rem] bg-layer-3/80 p-1">
|
<Tabs.List className="flex rounded bg-layer-3 p-1">
|
||||||
{enabledTabs.map((tab) => (
|
{enabledTabs.map((tab) => (
|
||||||
<Tabs.Trigger key={tab.key} value={tab.key} size="md" className="rounded-[0.875rem]">
|
<Tabs.Trigger key={tab.key} value={tab.key} size="md">
|
||||||
{tab.title}
|
{tab.title}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
))}
|
))}
|
||||||
<Tabs.Indicator />
|
<Tabs.Indicator />
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
<div className="vertical-scrollbar mt-4 scrollbar-sm flex-1 overflow-x-hidden overflow-y-auto px-1 pb-1">
|
<div className="vertical-scrollbar mt-3 scrollbar-sm flex-1 overflow-x-hidden overflow-y-auto p-3">
|
||||||
<Tabs.Content value="unsplash" className="h-full w-full space-y-4">
|
<Tabs.Content value="unsplash" className="h-full w-full space-y-4">
|
||||||
{(unsplashImages || !unsplashError) && (
|
{(unsplashImages || !unsplashError) && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -254,26 +254,21 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
|
||||||
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
placeholder={t("image_picker.search_placeholder")}
|
placeholder={t("image_picker.search_placeholder")}
|
||||||
className="nodedc-modal-input w-full !px-4 !py-3 text-13"
|
className="w-full text-13"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button variant="primary" size="xl" onClick={() => setSearchParams(formData.search)}>
|
||||||
variant="primary"
|
|
||||||
size="lg"
|
|
||||||
className="nodedc-modal-primary-button min-w-[8rem]"
|
|
||||||
onClick={() => setSearchParams(formData.search)}
|
|
||||||
>
|
|
||||||
{t("image_picker.search_button")}
|
{t("image_picker.search_button")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{unsplashImages ? (
|
{unsplashImages ? (
|
||||||
unsplashImages.length > 0 ? (
|
unsplashImages.length > 0 ? (
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
{unsplashImages.map((image) => (
|
{unsplashImages.map((image) => (
|
||||||
<div
|
<div
|
||||||
key={image.id}
|
key={image.id}
|
||||||
className="nodedc-modal-field relative col-span-2 aspect-video overflow-hidden rounded-[1rem] p-0 md:col-span-1"
|
className="relative col-span-2 aspect-video md:col-span-1"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
onChange(image.urls.regular);
|
onChange(image.urls.regular);
|
||||||
|
|
@ -282,7 +277,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
|
||||||
<img
|
<img
|
||||||
src={image.urls.small}
|
src={image.urls.small}
|
||||||
alt={image.alt_description}
|
alt={image.alt_description}
|
||||||
className="absolute inset-0 h-full w-full cursor-pointer rounded-[1rem] object-cover transition-transform duration-200 hover:scale-[1.02]"
|
className="absolute top-0 left-0 h-full w-full cursor-pointer rounded-sm object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -306,36 +301,36 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
|
||||||
)}
|
)}
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value="images" className="h-full w-full space-y-4">
|
<Tabs.Content value="images" className="h-full w-full space-y-4">
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
{Object.values(STATIC_COVER_IMAGES).map((imageUrl, index) => (
|
{Object.values(STATIC_COVER_IMAGES).map((imageUrl, index) => (
|
||||||
<div
|
<div
|
||||||
key={imageUrl}
|
key={imageUrl}
|
||||||
className="nodedc-modal-field relative col-span-2 aspect-video overflow-hidden rounded-[1rem] p-0 md:col-span-1"
|
className="relative col-span-2 aspect-video md:col-span-1"
|
||||||
onClick={() => handleStaticImageSelect(imageUrl)}
|
onClick={() => handleStaticImageSelect(imageUrl)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={t("image_picker.cover_image_alt", { index: index + 1 })}
|
alt={t("image_picker.cover_image_alt", { index: index + 1 })}
|
||||||
className="absolute inset-0 h-full w-full cursor-pointer rounded-[1rem] object-cover transition-transform duration-200 hover:scale-[1.02]"
|
className="absolute top-0 left-0 h-full w-full cursor-pointer rounded-sm object-cover transition-opacity hover:opacity-80"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content value="upload" className="h-full w-full">
|
<Tabs.Content value="upload" className="h-full w-full">
|
||||||
<div className="flex h-full w-full flex-col gap-y-3">
|
<div className="flex h-full w-full flex-col gap-y-2">
|
||||||
<div className="flex w-full flex-1 items-center gap-3">
|
<div className="flex w-full flex-1 items-center gap-3">
|
||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
className={`nodedc-modal-field relative grid h-full w-full cursor-pointer place-items-center overflow-hidden rounded-[1.35rem] p-6 text-center ${
|
className={`relative grid h-full w-full cursor-pointer place-items-center rounded-lg p-12 text-center focus:ring-2 focus:ring-accent-strong focus:ring-offset-2 focus:outline-none ${
|
||||||
(image === null && isDragActive) || !value
|
(image === null && isDragActive) || !value
|
||||||
? "border-2 border-dashed border-subtle/80 hover:bg-white/6"
|
? "border-2 border-dashed border-subtle hover:bg-surface-2"
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="nodedc-modal-chip absolute top-3 right-3 z-40 !min-h-0 !px-3 !py-1 text-11 font-medium"
|
className="absolute top-0 right-0 z-40 -translate-y-1/2 rounded-sm bg-surface-2 px-2 py-0.5 text-11 font-medium text-secondary"
|
||||||
>
|
>
|
||||||
{t("image_picker.edit")}
|
{t("image_picker.edit")}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -344,7 +339,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
|
||||||
<img
|
<img
|
||||||
src={image ? URL.createObjectURL(image) : getCoverImageDisplayURL(value, "")}
|
src={image ? URL.createObjectURL(image) : getCoverImageDisplayURL(value, "")}
|
||||||
alt="image"
|
alt="image"
|
||||||
className="h-full w-full rounded-[1rem] object-cover"
|
className="h-full w-full rounded-lg object-cover"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -368,20 +363,19 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
|
||||||
|
|
||||||
<p className="text-13 text-secondary">{t("image_picker.supported_formats")}</p>
|
<p className="text-13 text-secondary">{t("image_picker.supported_formats")}</p>
|
||||||
|
|
||||||
<div className="mt-auto flex items-start justify-end gap-3 border-t border-subtle/70 pt-4">
|
<div className="flex h-12 items-start justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setImage(null);
|
setImage(null);
|
||||||
}}
|
}}
|
||||||
className="nodedc-modal-secondary-button min-w-[8rem]"
|
|
||||||
>
|
>
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="nodedc-modal-primary-button min-w-[12rem]"
|
className="w-full"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!image}
|
disabled={!image}
|
||||||
loading={isImageUploading}
|
loading={isImageUploading}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +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 { cn } from "@plane/utils";
|
|
||||||
|
|
||||||
type HomeCardShellProps = {
|
|
||||||
title?: string;
|
|
||||||
eyebrow?: string;
|
|
||||||
description?: string;
|
|
||||||
action?: ReactNode;
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
contentClassName?: string;
|
|
||||||
tone?: "default" | "accent";
|
|
||||||
};
|
|
||||||
|
|
||||||
export function HomeCardShell(props: HomeCardShellProps) {
|
|
||||||
const { title, eyebrow, description, action, children, className, contentClassName, tone = "default" } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={cn("nodedc-home-card", className)} data-tone={tone}>
|
|
||||||
{(title || eyebrow || description || action) && (
|
|
||||||
<div className="relative z-[1] flex flex-wrap items-start justify-between gap-3 border-b border-white/6 px-5 py-5">
|
|
||||||
<div className="min-w-0 space-y-1">
|
|
||||||
{eyebrow && (
|
|
||||||
<div className="text-[11px] font-semibold tracking-[0.22em] text-placeholder uppercase">{eyebrow}</div>
|
|
||||||
)}
|
|
||||||
{title && <h3 className="text-18 font-semibold text-primary">{title}</h3>}
|
|
||||||
{description && <p className="max-w-2xl text-13 leading-5 text-secondary">{description}</p>}
|
|
||||||
</div>
|
|
||||||
{action && <div className="relative z-[1] flex items-center gap-2">{action}</div>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={cn("relative z-[1]", contentClassName ?? "p-5")}>{children}</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -4,15 +4,12 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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 { useTheme } from "next-themes";
|
||||||
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 { THomeWidgetKeys, THomeWidgetProps } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
// assets
|
// assets
|
||||||
import darkWidgetsAsset from "@/app/assets/empty-state/dashboard/widgets-dark.webp?url";
|
import darkWidgetsAsset from "@/app/assets/empty-state/dashboard/widgets-dark.webp?url";
|
||||||
import lightWidgetsAsset from "@/app/assets/empty-state/dashboard/widgets-light.webp?url";
|
import lightWidgetsAsset from "@/app/assets/empty-state/dashboard/widgets-light.webp?url";
|
||||||
|
|
@ -21,24 +18,13 @@ import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-ro
|
||||||
// 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 { WorkspaceService } from "@/services/workspace.service";
|
|
||||||
// local imports
|
// local imports
|
||||||
import { HomeCardShell } from "./home-card-shell";
|
|
||||||
import { HomeProjectInsights } from "./home-project-insights";
|
|
||||||
import { HomeProjectStack } from "./home-project-stack";
|
|
||||||
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 workspaceService = new WorkspaceService();
|
|
||||||
|
|
||||||
export const HOME_WIDGETS_LIST: {
|
export const HOME_WIDGETS_LIST: {
|
||||||
[key in THomeWidgetKeys]: {
|
[key in THomeWidgetKeys]: {
|
||||||
|
|
@ -74,205 +60,59 @@ export const HOME_WIDGETS_LIST: {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type DashboardWidgetsProps = {
|
export const DashboardWidgets = observer(function DashboardWidgets() {
|
||||||
currentUser?: IUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DashboardWidgets = observer(function DashboardWidgets(props: DashboardWidgetsProps) {
|
|
||||||
const { currentUser } = props;
|
|
||||||
// router
|
// router
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
const workspaceSlugValue = Array.isArray(workspaceSlug) ? workspaceSlug[0] : workspaceSlug?.toString();
|
|
||||||
// navigation
|
// navigation
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
// theme hook
|
// theme hook
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { toggleWidgetSettings, widgetsMap, showWidgetSettings, loading } = useHome();
|
const { toggleWidgetSettings, widgetsMap, showWidgetSettings, orderedWidgets, isAnyWidgetEnabled, loading } =
|
||||||
const { loader, joinedProjectIds, getPartialProjectById, fetchProjectAnalyticsCount, getProjectAnalyticsCountById } =
|
useHome();
|
||||||
useProject();
|
const { loader } = useProject();
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
// plane hooks
|
// plane hooks
|
||||||
const { t, currentLocale } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// states
|
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
|
||||||
// derived values
|
// derived values
|
||||||
const noWidgetsResolvedPath = resolvedTheme === "light" ? lightWidgetsAsset : darkWidgetsAsset;
|
const noWidgetsResolvedPath = resolvedTheme === "light" ? lightWidgetsAsset : darkWidgetsAsset;
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const isWikiApp = workspaceSlugValue ? pathname.includes(`/${workspaceSlugValue}/pages`) : false;
|
const isWikiApp = pathname.includes(`/${workspaceSlug.toString()}/pages`);
|
||||||
|
if (!workspaceSlug) return null;
|
||||||
const projectIds = joinedProjectIds ?? [];
|
|
||||||
|
|
||||||
const { data: detailedProjects } = useSWR(
|
|
||||||
workspaceSlugValue && projectIds.length > 0 ? `HOME_PROJECT_DETAILS_${workspaceSlugValue}` : null,
|
|
||||||
() => projectService.getProjects(workspaceSlugValue!),
|
|
||||||
{
|
|
||||||
revalidateIfStale: false,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
useSWR(
|
|
||||||
workspaceSlugValue && projectIds.length > 0
|
|
||||||
? `HOME_PROJECT_STATS_${workspaceSlugValue}_${projectIds.join(",")}`
|
|
||||||
: null,
|
|
||||||
() =>
|
|
||||||
fetchProjectAnalyticsCount(workspaceSlugValue!, {
|
|
||||||
project_ids: projectIds.join(","),
|
|
||||||
fields: "total_issues,completed_issues,total_members,total_cycles,total_modules",
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
revalidateIfStale: false,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: workspaceRecents } = useSWR(
|
|
||||||
workspaceSlugValue ? `WORKSPACE_RECENT_ACTIVITY_${workspaceSlugValue}_all item` : null,
|
|
||||||
() => workspaceService.fetchWorkspaceRecents(workspaceSlugValue!),
|
|
||||||
{
|
|
||||||
revalidateIfStale: false,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const detailedProjectsMap = new Map((detailedProjects ?? []).map((project) => [project.id, project]));
|
|
||||||
const homeProjects = projectIds
|
|
||||||
.map(
|
|
||||||
(projectId) =>
|
|
||||||
(detailedProjectsMap.get(projectId) ?? getPartialProjectById(projectId)) as THomeProjectData | undefined
|
|
||||||
)
|
|
||||||
.filter((project): project is THomeProjectData => !!project);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (homeProjects.length === 0) {
|
|
||||||
if (selectedProjectId !== null) setSelectedProjectId(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedProjectId || !homeProjects.some((project) => project.id === selectedProjectId)) {
|
|
||||||
setSelectedProjectId(homeProjects[0].id);
|
|
||||||
}
|
|
||||||
}, [homeProjects, selectedProjectId]);
|
|
||||||
|
|
||||||
const analyticsMap = projectIds.reduce<Record<string, TProjectAnalyticsCount | undefined>>((acc, projectId) => {
|
|
||||||
acc[projectId] = getProjectAnalyticsCountById(projectId);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
const analyticsCollection = projectIds
|
|
||||||
.map((projectId) => analyticsMap[projectId])
|
|
||||||
.filter((item): item is TProjectAnalyticsCount => !!item);
|
|
||||||
const selectedProject = homeProjects.find((project) => project.id === selectedProjectId);
|
|
||||||
const selectedProjectAnalytics =
|
|
||||||
(selectedProjectId ? analyticsMap[selectedProjectId] : undefined) ?? aggregateProjectAnalytics(analyticsCollection);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
if (!workspaceSlugValue) return null;
|
|
||||||
if (loading || loader !== "loaded") return <HomeLoader />;
|
if (loading || loader !== "loaded") return <HomeLoader />;
|
||||||
|
|
||||||
const recentsCard = isRecentsEnabled ? (
|
|
||||||
<HomeCardShell className="overflow-hidden" contentClassName="p-5">
|
|
||||||
<RecentActivityWidget
|
|
||||||
workspaceSlug={workspaceSlugValue}
|
|
||||||
recents={workspaceRecents}
|
|
||||||
projectId={selectedProjectId}
|
|
||||||
/>
|
|
||||||
</HomeCardShell>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const sideWidgetCards = [
|
|
||||||
isQuickLinksEnabled ? (
|
|
||||||
<HomeCardShell key="quick_links" className="overflow-hidden" contentClassName="p-5">
|
|
||||||
<DashboardQuickLinks workspaceSlug={workspaceSlugValue} />
|
|
||||||
</HomeCardShell>
|
|
||||||
) : null,
|
|
||||||
isStickiesEnabled ? (
|
|
||||||
<HomeCardShell key="my_stickies" className="overflow-hidden" contentClassName="p-5">
|
|
||||||
<StickiesWidget />
|
|
||||||
</HomeCardShell>
|
|
||||||
) : null,
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
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-7">
|
||||||
<HomePageHeader />
|
<HomePageHeader />
|
||||||
<ManageWidgetsModal
|
<ManageWidgetsModal
|
||||||
workspaceSlug={workspaceSlugValue}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
isModalOpen={showWidgetSettings}
|
isModalOpen={showWidgetSettings}
|
||||||
handleOnClose={() => toggleWidgetSettings(false)}
|
handleOnClose={() => toggleWidgetSettings(false)}
|
||||||
/>
|
/>
|
||||||
|
{!isWikiApp && <NoProjectsEmptyState />}
|
||||||
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[340px_minmax(0,1fr)]">
|
{isAnyWidgetEnabled ? (
|
||||||
<div className="min-w-0">
|
<div className="flex flex-col gap-2">
|
||||||
<HomeProjectStack
|
{orderedWidgets.map((key) => {
|
||||||
projects={homeProjects}
|
const WidgetComponent = HOME_WIDGETS_LIST[key]?.component;
|
||||||
analyticsMap={analyticsMap}
|
const isEnabled = widgetsMap[key]?.is_enabled;
|
||||||
recents={workspaceRecents}
|
if (!WidgetComponent || !isEnabled) return null;
|
||||||
selectedProjectId={selectedProjectId}
|
return (
|
||||||
onSelectProject={setSelectedProjectId}
|
<div key={key} className="py-3">
|
||||||
workspaceSlug={workspaceSlugValue}
|
<WidgetComponent workspaceSlug={workspaceSlug.toString()} />
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 space-y-5">
|
|
||||||
{currentUser && (
|
|
||||||
<UserGreetingsView
|
|
||||||
user={currentUser}
|
|
||||||
workspaceName={currentWorkspace?.name}
|
|
||||||
selectedProject={selectedProject}
|
|
||||||
selectedProjectAnalytics={selectedProjectAnalytics}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<HomeProjectInsights
|
|
||||||
project={selectedProject}
|
|
||||||
analytics={selectedProjectAnalytics}
|
|
||||||
analyticsCollection={analyticsCollection}
|
|
||||||
recents={workspaceRecents}
|
|
||||||
locale={currentLocale || "ru-RU"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!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>
|
</div>
|
||||||
</HomeCardShell>
|
);
|
||||||
)}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div className="grid h-full w-full place-items-center">
|
||||||
|
<SimpleEmptyState
|
||||||
|
title={t("home.empty.widgets.title")}
|
||||||
|
description={t("home.empty.widgets.description")}
|
||||||
|
assetPath={noWidgetsResolvedPath}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,367 +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 { 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,
|
|
||||||
getCompletionRate,
|
|
||||||
type THomeProjectData,
|
|
||||||
} from "./home.utils";
|
|
||||||
|
|
||||||
type HomeProjectInsightsProps = {
|
|
||||||
project?: THomeProjectData;
|
|
||||||
analytics?: TProjectAnalyticsCount;
|
|
||||||
analyticsCollection?: TProjectAnalyticsCount[];
|
|
||||||
recents?: TActivityEntityData[];
|
|
||||||
locale: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TActivityPoint = {
|
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 maxValue = Math.max(...data.map((item) => item.value), 1);
|
|
||||||
const stepX = data.length > 1 ? (width - paddingX * 2) / (data.length - 1) : 0;
|
|
||||||
|
|
||||||
const points = data.map((item, index) => {
|
|
||||||
const x = paddingX + index * stepX;
|
|
||||||
const y = height - paddingY - (item.value / maxValue) * (height - paddingY * 2);
|
|
||||||
return { x, y };
|
|
||||||
});
|
|
||||||
|
|
||||||
const linePath = points
|
|
||||||
.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x.toFixed(2)} ${point.y.toFixed(2)}`)
|
|
||||||
.join(" ");
|
|
||||||
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`;
|
|
||||||
|
|
||||||
return { width, height, paddingY, points, areaPath, linePath, maxValue };
|
|
||||||
};
|
|
||||||
|
|
||||||
export function HomeProjectInsights(props: HomeProjectInsightsProps) {
|
|
||||||
const { project, analytics, analyticsCollection, recents, locale } = props;
|
|
||||||
const chartId = useId();
|
|
||||||
|
|
||||||
const resolvedAnalytics = analytics ?? aggregateProjectAnalytics(analyticsCollection);
|
|
||||||
const totalIssues = resolvedAnalytics?.total_issues ?? 0;
|
|
||||||
const completedIssues = resolvedAnalytics?.completed_issues ?? 0;
|
|
||||||
const openIssues = Math.max(totalIssues - completedIssues, 0);
|
|
||||||
const completionRate = getCompletionRate(resolvedAnalytics);
|
|
||||||
|
|
||||||
const activitySeries = useMemo<TActivityPoint[]>(() => {
|
|
||||||
const formatter = new Intl.DateTimeFormat(locale || "ru-RU", {
|
|
||||||
weekday: "short",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
const series = Array.from({ length: 7 }, (_, index) => {
|
|
||||||
const date = new Date();
|
|
||||||
date.setDate(date.getDate() - (6 - index));
|
|
||||||
const key = date.toISOString().slice(0, 10);
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
label: formatter.format(date),
|
|
||||||
value: 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const activity of recents ?? []) {
|
|
||||||
const projectId = getActivityProjectId(activity);
|
|
||||||
if (project && projectId !== project.id) continue;
|
|
||||||
if (!project && projectId === null && activity.entity_name === "workspace_page") continue;
|
|
||||||
|
|
||||||
const activityKey = new Date(activity.visited_at).toISOString().slice(0, 10);
|
|
||||||
const matchingPoint = series.find((point) => point.key === activityKey);
|
|
||||||
if (matchingPoint) matchingPoint.value += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return series;
|
|
||||||
}, [locale, project, recents]);
|
|
||||||
|
|
||||||
const chart = buildChartPaths(activitySeries);
|
|
||||||
const recentTouchpoints = activitySeries.reduce((sum, item) => sum + item.value, 0);
|
|
||||||
const benchmark = {
|
|
||||||
members: Math.max(
|
|
||||||
...(analyticsCollection ?? []).map((item) => item.total_members ?? 0),
|
|
||||||
resolvedAnalytics?.total_members ?? 0,
|
|
||||||
1
|
|
||||||
),
|
|
||||||
cycles: Math.max(
|
|
||||||
...(analyticsCollection ?? []).map((item) => item.total_cycles ?? 0),
|
|
||||||
resolvedAnalytics?.total_cycles ?? 0,
|
|
||||||
1
|
|
||||||
),
|
|
||||||
modules: Math.max(
|
|
||||||
...(analyticsCollection ?? []).map((item) => item.total_modules ?? 0),
|
|
||||||
resolvedAnalytics?.total_modules ?? 0,
|
|
||||||
1
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const metricCards = [
|
|
||||||
{
|
|
||||||
label: "Готовность",
|
|
||||||
value: `${completionRate}%`,
|
|
||||||
caption: `${completedIssues} из ${totalIssues || 0} закрыто`,
|
|
||||||
icon: <CheckCircle2 className="size-4" />,
|
|
||||||
accent: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Открытые задачи",
|
|
||||||
value: formatCompactNumber(openIssues),
|
|
||||||
caption: "Текущая незакрытая нагрузка",
|
|
||||||
icon: <Layers3 className="size-4" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Касания за 7 дней",
|
|
||||||
value: formatCompactNumber(recentTouchpoints),
|
|
||||||
caption: "Recent activity по этому фокусу",
|
|
||||||
icon: <Activity className="size-4" />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const progressRows = [
|
|
||||||
{
|
|
||||||
label: "Команда",
|
|
||||||
value: resolvedAnalytics?.total_members ?? 0,
|
|
||||||
max: benchmark.members,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Циклы",
|
|
||||||
value: resolvedAnalytics?.total_cycles ?? 0,
|
|
||||||
max: benchmark.cycles,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Модули",
|
|
||||||
value: resolvedAnalytics?.total_modules ?? 0,
|
|
||||||
max: benchmark.modules,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HomeCardShell
|
|
||||||
eyebrow={project ? "Фокус проекта" : "Workspace overview"}
|
|
||||||
title={project ? project.name : "Координационный обзор workspace"}
|
|
||||||
description={
|
|
||||||
project
|
|
||||||
? `${project.identifier} ${project.description ? `• ${project.description}` : "• домашняя сводка проекта в одном экране"}`
|
|
||||||
: "Агрегированный обзор по текущим проектам, recent activity и операционной нагрузке."
|
|
||||||
}
|
|
||||||
tone="default"
|
|
||||||
>
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[minmax(0,1.2fr)_minmax(260px,0.8fr)]">
|
|
||||||
<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">
|
|
||||||
<div className="mb-4 flex items-center justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<div className="text-14 font-semibold text-primary">Темп активности</div>
|
|
||||||
<div className="text-12 text-secondary">Последние 7 дней переходов и взаимодействий внутри сводки.</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-full bg-white/6 px-3 py-1.5 text-12 text-secondary">
|
|
||||||
{recentTouchpoints} событий
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
{["col-1", "col-2", "col-3", "col-4"].map((key) => (
|
|
||||||
<div key={key} className="border-r border-dashed border-white/8 last:border-r-0" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
viewBox={`0 0 ${chart.width} ${chart.height}`}
|
|
||||||
className="relative z-[1] h-[180px] w-full"
|
|
||||||
preserveAspectRatio="none"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<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="100%" stopColor="rgba(var(--nodedc-accent-rgb),0.02)" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
{activitySeries.map((point, index) => {
|
|
||||||
const x = chart.points[index]?.x ?? 0;
|
|
||||||
return (
|
|
||||||
<line
|
|
||||||
key={point.key}
|
|
||||||
x1={x}
|
|
||||||
x2={x}
|
|
||||||
y1={12}
|
|
||||||
y2={chart.height - chart.paddingY}
|
|
||||||
stroke="rgba(255,255,255,0.05)"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{[0.25, 0.5, 0.75].map((position) => {
|
|
||||||
const y = chart.height - chart.paddingY - position * (chart.height - chart.paddingY * 2);
|
|
||||||
return (
|
|
||||||
<line
|
|
||||||
key={position}
|
|
||||||
x1={10}
|
|
||||||
x2={chart.width - 10}
|
|
||||||
y1={y}
|
|
||||||
y2={y}
|
|
||||||
stroke="rgba(255,255,255,0.05)"
|
|
||||||
strokeDasharray="4 6"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<path d={chart.areaPath} fill={`url(#${chartId}-fill)`} />
|
|
||||||
<path
|
|
||||||
d={chart.linePath}
|
|
||||||
fill="none"
|
|
||||||
stroke="rgb(var(--nodedc-accent-rgb))"
|
|
||||||
strokeWidth="4"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
{activitySeries.map((activityPoint, index) => {
|
|
||||||
const point = chart.points[index];
|
|
||||||
if (!point) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<circle
|
|
||||||
key={activityPoint.key}
|
|
||||||
cx={point.x}
|
|
||||||
cy={point.y}
|
|
||||||
r="5"
|
|
||||||
fill="rgb(var(--nodedc-accent-rgb))"
|
|
||||||
stroke="rgba(9,9,12,0.8)"
|
|
||||||
strokeWidth="3"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<div className="relative z-[1] mt-4 grid grid-cols-7 gap-2">
|
|
||||||
{activitySeries.map((point) => (
|
|
||||||
<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="mt-1 text-13 font-semibold text-primary">{point.value}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<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="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" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-13 font-semibold text-primary">Операционный срез</div>
|
|
||||||
<div className="text-12 text-secondary">
|
|
||||||
Нагрузка команды, циклов и модулей относительно остального workspace.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 space-y-4">
|
|
||||||
{progressRows.map((row) => {
|
|
||||||
const percent = row.max > 0 ? Math.max((row.value / row.max) * 100, row.value > 0 ? 10 : 0) : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={row.label} className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between gap-3 text-12">
|
|
||||||
<span className="text-secondary">{row.label}</span>
|
|
||||||
<span className="font-semibold text-primary">{row.value}</span>
|
|
||||||
</div>
|
|
||||||
<div className="nodedc-home-progress-track">
|
|
||||||
<div className="nodedc-home-progress-fill" style={{ width: `${Math.min(percent, 100)}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-[28px] border border-white/6 bg-black/12 p-5">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<div className="text-13 font-semibold text-primary">Ритм исполнения</div>
|
|
||||||
<div className="text-12 text-secondary">Сколько уже закрыто и какой объём ещё держим открытым.</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-full bg-white/6 px-3 py-1.5 text-12 text-secondary">{completionRate}%</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 space-y-4">
|
|
||||||
<div className="rounded-[22px] bg-white/4 p-4">
|
|
||||||
<div className="flex items-center justify-between gap-3 text-12">
|
|
||||||
<span className="text-secondary">Закрытые задачи</span>
|
|
||||||
<span className="font-semibold text-primary">{completedIssues}</span>
|
|
||||||
</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="rounded-[22px] bg-white/4 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="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> держит </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>
|
|
||||||
</HomeCardShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,233 +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 Link from "next/link";
|
|
||||||
import { ArrowUpRight, FolderOpenDot, Layers3, UsersRound } from "lucide-react";
|
|
||||||
import type { TActivityEntityData, TProjectAnalyticsCount } from "@plane/types";
|
|
||||||
import { Logo } from "@plane/propel/emoji-icon-picker";
|
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
import { CoverImage } from "@/components/common/cover-image";
|
|
||||||
import { HomeCardShell } from "./home-card-shell";
|
|
||||||
import { getActivityProjectId, getCompletionRate, type THomeProjectData } from "./home.utils";
|
|
||||||
|
|
||||||
type HomeProjectStackProps = {
|
|
||||||
projects: THomeProjectData[];
|
|
||||||
analyticsMap: Record<string, TProjectAnalyticsCount | undefined>;
|
|
||||||
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;
|
|
||||||
|
|
||||||
export function HomeProjectStack(props: HomeProjectStackProps) {
|
|
||||||
const { projects, analyticsMap, recents, selectedProjectId, onSelectProject, workspaceSlug } = props;
|
|
||||||
|
|
||||||
const activeProject = projects.find((project: THomeProjectData) => project.id === selectedProjectId);
|
|
||||||
const orderedProjects = activeProject
|
|
||||||
? [activeProject, ...projects.filter((project: THomeProjectData) => project.id !== activeProject.id)]
|
|
||||||
: projects;
|
|
||||||
|
|
||||||
const visibleProjects = orderedProjects.slice(0, STACK_VISIBLE_LIMIT);
|
|
||||||
const activityCountByProject = (recents ?? []).reduce<Record<string, number>>((acc, activity) => {
|
|
||||||
const projectId = getActivityProjectId(activity);
|
|
||||||
if (!projectId) return acc;
|
|
||||||
|
|
||||||
acc[projectId] = (acc[projectId] ?? 0) + 1;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
if (projects.length === 0) {
|
|
||||||
return (
|
|
||||||
<HomeCardShell
|
|
||||||
eyebrow="Workspace"
|
|
||||||
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="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" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-15 font-semibold text-primary">Пока нет проектов для сводки</div>
|
|
||||||
<div className="text-13 text-secondary">
|
|
||||||
Откройте quickstart ниже и создайте первый проект для этой панели.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</HomeCardShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HomeCardShell
|
|
||||||
eyebrow="Workspace"
|
|
||||||
title="Доступные проекты"
|
|
||||||
description="Нажатие на карточку проекта перестраивает домашнюю сводку, recent activity и аналитический фокус справа."
|
|
||||||
tone="accent"
|
|
||||||
action={
|
|
||||||
selectedProjectPath ? (
|
|
||||||
<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 absolute inset-x-0 text-left", {
|
|
||||||
"cursor-default": isActive,
|
|
||||||
})}
|
|
||||||
data-active={isActive}
|
|
||||||
onClick={() => onSelectProject(project.id)}
|
|
||||||
style={{
|
|
||||||
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/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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-[24px] border border-white/6 bg-black/10 p-4">
|
|
||||||
<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/6 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 (
|
|
||||||
<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>
|
|
||||||
</HomeCardShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +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 {
|
|
||||||
IPartialProject,
|
|
||||||
IProject,
|
|
||||||
TActivityEntityData,
|
|
||||||
TIssueEntityData,
|
|
||||||
TPageEntityData,
|
|
||||||
TProjectAnalyticsCount,
|
|
||||||
TProjectEntityData,
|
|
||||||
} from "@plane/types";
|
|
||||||
|
|
||||||
export type THomeProjectData = Pick<IPartialProject, "id" | "name" | "identifier" | "logo_props" | "member_role"> &
|
|
||||||
Partial<Pick<IProject, "cover_image_url" | "description">>;
|
|
||||||
|
|
||||||
export const getActivityProjectId = (activity: TActivityEntityData): string | null => {
|
|
||||||
if (!activity?.entity_data) return null;
|
|
||||||
|
|
||||||
switch (activity.entity_name) {
|
|
||||||
case "project":
|
|
||||||
return (activity.entity_data as TProjectEntityData).id ?? null;
|
|
||||||
case "issue":
|
|
||||||
return (activity.entity_data as TIssueEntityData).project_id ?? null;
|
|
||||||
case "page":
|
|
||||||
case "workspace_page":
|
|
||||||
return (activity.entity_data as TPageEntityData).project_id ?? null;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCompletionRate = (analytics?: TProjectAnalyticsCount): number => {
|
|
||||||
const totalIssues = analytics?.total_issues ?? 0;
|
|
||||||
const completedIssues = analytics?.completed_issues ?? 0;
|
|
||||||
|
|
||||||
if (totalIssues === 0) return 0;
|
|
||||||
|
|
||||||
return Math.round((completedIssues / totalIssues) * 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const aggregateProjectAnalytics = (
|
|
||||||
analytics: TProjectAnalyticsCount[] | undefined
|
|
||||||
): TProjectAnalyticsCount | undefined => {
|
|
||||||
if (!analytics || analytics.length === 0) return undefined;
|
|
||||||
|
|
||||||
return analytics.reduce<TProjectAnalyticsCount>(
|
|
||||||
(acc, item) => ({
|
|
||||||
id: "workspace-overview",
|
|
||||||
total_issues: (acc.total_issues ?? 0) + (item.total_issues ?? 0),
|
|
||||||
completed_issues: (acc.completed_issues ?? 0) + (item.completed_issues ?? 0),
|
|
||||||
total_members: (acc.total_members ?? 0) + (item.total_members ?? 0),
|
|
||||||
total_cycles: (acc.total_cycles ?? 0) + (item.total_cycles ?? 0),
|
|
||||||
total_modules: (acc.total_modules ?? 0) + (item.total_modules ?? 0),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
id: "workspace-overview",
|
|
||||||
total_issues: 0,
|
|
||||||
completed_issues: 0,
|
|
||||||
total_members: 0,
|
|
||||||
total_cycles: 0,
|
|
||||||
total_modules: 0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { HomePeekOverviewsRoot } from "@/plane-web/components/home";
|
||||||
import { TourRoot } from "@/plane-web/components/onboarding/tour/root";
|
import { TourRoot } from "@/plane-web/components/onboarding/tour/root";
|
||||||
// local imports
|
// local imports
|
||||||
import { DashboardWidgets } from "./home-dashboard-widgets";
|
import { DashboardWidgets } from "./home-dashboard-widgets";
|
||||||
|
import { UserGreetingsView } from "./user-greetings";
|
||||||
|
|
||||||
// Temporary NodeDC toggle: keep product tour implementation in code,
|
// Temporary NodeDC toggle: keep product tour implementation in code,
|
||||||
// but do not show it in the local PoC until the onboarding flow is revisited.
|
// but do not show it in the local PoC until the onboarding flow is revisited.
|
||||||
|
|
@ -58,8 +59,9 @@ export const WorkspaceHomeView = observer(function WorkspaceHomeView() {
|
||||||
<>
|
<>
|
||||||
<HomePeekOverviewsRoot />
|
<HomePeekOverviewsRoot />
|
||||||
<ContentWrapper className="mx-auto scrollbar-hide gap-6 bg-transparent px-page-x">
|
<ContentWrapper className="mx-auto scrollbar-hide gap-6 bg-transparent px-page-x">
|
||||||
<div className="nodedc-workspace-page-shell mx-auto w-full max-w-[1480px]">
|
<div className="nodedc-workspace-page-shell mx-auto w-full max-w-[980px]">
|
||||||
<DashboardWidgets currentUser={currentUser} />
|
{currentUser && <UserGreetingsView user={currentUser} />}
|
||||||
|
<DashboardWidgets />
|
||||||
</div>
|
</div>
|
||||||
</ContentWrapper>
|
</ContentWrapper>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,17 @@
|
||||||
|
|
||||||
// plane types
|
// plane types
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import type { IUser, TProjectAnalyticsCount } from "@plane/types";
|
import type { IUser } from "@plane/types";
|
||||||
// plane ui
|
// plane ui
|
||||||
// hooks
|
// 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";
|
|
||||||
|
|
||||||
export interface IUserGreetingsView {
|
export interface IUserGreetingsView {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
workspaceName?: string | null;
|
|
||||||
selectedProject?: THomeProjectData;
|
|
||||||
selectedProjectAnalytics?: TProjectAnalyticsCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserGreetingsView(props: IUserGreetingsView) {
|
export function UserGreetingsView(props: IUserGreetingsView) {
|
||||||
const { user, workspaceName, selectedProject, selectedProjectAnalytics } = props;
|
const { user } = props;
|
||||||
// current time hook
|
// current time hook
|
||||||
const { currentTime } = useCurrentTime();
|
const { currentTime } = useCurrentTime();
|
||||||
// store hooks
|
// store hooks
|
||||||
|
|
@ -49,52 +44,18 @@ export function UserGreetingsView(props: IUserGreetingsView) {
|
||||||
}).format(currentTime);
|
}).format(currentTime);
|
||||||
|
|
||||||
const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening";
|
const greeting = parseInt(hour, 10) < 12 ? "morning" : parseInt(hour, 10) < 18 ? "afternoon" : "evening";
|
||||||
const completionRate = getCompletionRate(selectedProjectAnalytics);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HomeCardShell
|
<div className="my-6 flex flex-col items-center">
|
||||||
tone="accent"
|
<h2 className="text-center text-20 font-semibold">
|
||||||
eyebrow={workspaceName ?? "Workspace Home"}
|
{t("good")} {t(greeting)}, {user?.first_name} {user?.last_name}
|
||||||
title={`${t("good")} ${t(greeting)}, ${user?.first_name} ${user?.last_name}`}
|
</h2>
|
||||||
description={`${weekDay}, ${date} ${timeString}`}
|
<h5 className="flex items-center gap-2 font-medium text-placeholder">
|
||||||
contentClassName="p-5"
|
<div>{greeting === "morning" ? "🌤️" : greeting === "afternoon" ? "🌥️" : "🌙️"}</div>
|
||||||
>
|
<div>
|
||||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]">
|
{weekDay}, {date} {timeString}
|
||||||
<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>
|
||||||
|
</h5>
|
||||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-1">
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<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 ? `${completionRate}%` : "—"}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-12 text-secondary">
|
|
||||||
{selectedProject
|
|
||||||
? "Закрытые задачи относительно общего объёма."
|
|
||||||
: "Станет доступен после выбора проекта."}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</HomeCardShell>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
|
@ -16,7 +16,6 @@ import type { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys }
|
||||||
import { ContentOverflowWrapper } from "@/components/core/content-overflow-HOC";
|
import { ContentOverflowWrapper } from "@/components/core/content-overflow-HOC";
|
||||||
// plane web services
|
// plane web services
|
||||||
import { WorkspaceService } from "@/services/workspace.service";
|
import { WorkspaceService } from "@/services/workspace.service";
|
||||||
import { getActivityProjectId } from "../../home.utils";
|
|
||||||
import { RecentsEmptyState } from "../empty-states";
|
import { RecentsEmptyState } from "../empty-states";
|
||||||
import { EWidgetKeys, WidgetLoader } from "../loaders";
|
import { EWidgetKeys, WidgetLoader } from "../loaders";
|
||||||
import { FiltersDropdown } from "./filters";
|
import { FiltersDropdown } from "./filters";
|
||||||
|
|
@ -36,20 +35,18 @@ const filters: { name: TRecentActivityFilterKeys; icon?: React.ReactNode; i18n_k
|
||||||
type TRecentWidgetProps = THomeWidgetProps & {
|
type TRecentWidgetProps = THomeWidgetProps & {
|
||||||
presetFilter?: TRecentActivityFilterKeys;
|
presetFilter?: TRecentActivityFilterKeys;
|
||||||
showFilterSelect?: boolean;
|
showFilterSelect?: boolean;
|
||||||
projectId?: string | null;
|
|
||||||
recents?: TActivityEntityData[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RecentActivityWidget = observer(function RecentActivityWidget(props: TRecentWidgetProps) {
|
export const RecentActivityWidget = observer(function RecentActivityWidget(props: TRecentWidgetProps) {
|
||||||
const { presetFilter, showFilterSelect = true, workspaceSlug, projectId, recents: preloadedRecents } = props;
|
const { presetFilter, showFilterSelect = true, workspaceSlug } = props;
|
||||||
// states
|
// states
|
||||||
const [filter, setFilter] = useState<TRecentActivityFilterKeys>(presetFilter ?? filters[0].name);
|
const [filter, setFilter] = useState<TRecentActivityFilterKeys>(presetFilter ?? filters[0].name);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// ref
|
// ref
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { data: fetchedRecents, isLoading } = useSWR(
|
const { data: recents, isLoading } = useSWR(
|
||||||
workspaceSlug && !preloadedRecents ? `WORKSPACE_RECENT_ACTIVITY_${workspaceSlug}_${filter}` : null,
|
workspaceSlug ? `WORKSPACE_RECENT_ACTIVITY_${workspaceSlug}_${filter}` : null,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
? () =>
|
? () =>
|
||||||
workspaceService.fetchWorkspaceRecents(
|
workspaceService.fetchWorkspaceRecents(
|
||||||
|
|
@ -64,19 +61,6 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const recents = useMemo(() => {
|
|
||||||
const source = preloadedRecents ?? fetchedRecents ?? [];
|
|
||||||
const filteredByType = source.filter((activity) =>
|
|
||||||
filter === filters[0].name ? true : activity.entity_name === filter
|
|
||||||
);
|
|
||||||
|
|
||||||
return filteredByType.filter((activity) => {
|
|
||||||
if (!activity.entity_data) return false;
|
|
||||||
if (!projectId) return true;
|
|
||||||
return getActivityProjectId(activity) === projectId;
|
|
||||||
});
|
|
||||||
}, [fetchedRecents, filter, preloadedRecents, projectId]);
|
|
||||||
|
|
||||||
const resolveRecent = (activity: TActivityEntityData) => {
|
const resolveRecent = (activity: TActivityEntityData) => {
|
||||||
switch (activity.entity_name) {
|
switch (activity.entity_name) {
|
||||||
case "page":
|
case "page":
|
||||||
|
|
@ -91,7 +75,7 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isLoading && recents.length === 0)
|
if (!isLoading && recents?.length === 0)
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="max-h-[500px] overflow-y-scroll">
|
<div ref={ref} className="max-h-[500px] overflow-y-scroll">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
|
@ -117,7 +101,10 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-[250px] flex-col">
|
<div className="flex min-h-[250px] flex-col">
|
||||||
{isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />}
|
{isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />}
|
||||||
{!isLoading && recents.map((activity) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
|
{!isLoading &&
|
||||||
|
recents
|
||||||
|
?.filter((recent) => recent.entity_data)
|
||||||
|
.map((activity) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
|
||||||
</div>
|
</div>
|
||||||
</ContentOverflowWrapper>
|
</ContentOverflowWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,33 @@
|
||||||
import type { EFileAssetType } from "@plane/types";
|
import type { EFileAssetType } from "@plane/types";
|
||||||
import { getFileURL } from "@plane/utils";
|
import { getFileURL } from "@plane/utils";
|
||||||
|
|
||||||
import CoverImage1 from "@/app/assets/cover-images/image_1.png?url";
|
import CoverImage1 from "@/app/assets/cover-images/image_1.jpg?url";
|
||||||
import CoverImage10 from "@/app/assets/cover-images/image_10.jpg?url";
|
import CoverImage10 from "@/app/assets/cover-images/image_10.jpg?url";
|
||||||
import CoverImage11 from "@/app/assets/cover-images/image_11.png?url";
|
import CoverImage11 from "@/app/assets/cover-images/image_11.jpg?url";
|
||||||
import CoverImage12 from "@/app/assets/cover-images/image_12.jpg?url";
|
import CoverImage12 from "@/app/assets/cover-images/image_12.jpg?url";
|
||||||
|
import CoverImage13 from "@/app/assets/cover-images/image_13.jpg?url";
|
||||||
|
import CoverImage14 from "@/app/assets/cover-images/image_14.jpg?url";
|
||||||
|
import CoverImage15 from "@/app/assets/cover-images/image_15.jpg?url";
|
||||||
|
import CoverImage16 from "@/app/assets/cover-images/image_16.jpg?url";
|
||||||
|
import CoverImage17 from "@/app/assets/cover-images/image_17.jpg?url";
|
||||||
|
import CoverImage18 from "@/app/assets/cover-images/image_18.jpg?url";
|
||||||
|
import CoverImage19 from "@/app/assets/cover-images/image_19.jpg?url";
|
||||||
import CoverImage2 from "@/app/assets/cover-images/image_2.jpg?url";
|
import CoverImage2 from "@/app/assets/cover-images/image_2.jpg?url";
|
||||||
|
import CoverImage20 from "@/app/assets/cover-images/image_20.jpg?url";
|
||||||
|
import CoverImage21 from "@/app/assets/cover-images/image_21.jpg?url";
|
||||||
|
import CoverImage22 from "@/app/assets/cover-images/image_22.jpg?url";
|
||||||
|
import CoverImage23 from "@/app/assets/cover-images/image_23.jpg?url";
|
||||||
|
import CoverImage24 from "@/app/assets/cover-images/image_24.jpg?url";
|
||||||
|
import CoverImage25 from "@/app/assets/cover-images/image_25.jpg?url";
|
||||||
|
import CoverImage26 from "@/app/assets/cover-images/image_26.jpg?url";
|
||||||
|
import CoverImage27 from "@/app/assets/cover-images/image_27.jpg?url";
|
||||||
|
import CoverImage28 from "@/app/assets/cover-images/image_28.jpg?url";
|
||||||
|
import CoverImage29 from "@/app/assets/cover-images/image_29.jpg?url";
|
||||||
import CoverImage3 from "@/app/assets/cover-images/image_3.jpg?url";
|
import CoverImage3 from "@/app/assets/cover-images/image_3.jpg?url";
|
||||||
import CoverImage4 from "@/app/assets/cover-images/image_4.jpg?url";
|
import CoverImage4 from "@/app/assets/cover-images/image_4.jpg?url";
|
||||||
import CoverImage5 from "@/app/assets/cover-images/image_5.jpg?url";
|
import CoverImage5 from "@/app/assets/cover-images/image_5.jpg?url";
|
||||||
import CoverImage6 from "@/app/assets/cover-images/image_6.webp?url";
|
import CoverImage6 from "@/app/assets/cover-images/image_6.jpg?url";
|
||||||
import CoverImage7 from "@/app/assets/cover-images/image_7.webp?url";
|
import CoverImage7 from "@/app/assets/cover-images/image_7.jpg?url";
|
||||||
import CoverImage8 from "@/app/assets/cover-images/image_8.jpg?url";
|
import CoverImage8 from "@/app/assets/cover-images/image_8.jpg?url";
|
||||||
import CoverImage9 from "@/app/assets/cover-images/image_9.jpg?url";
|
import CoverImage9 from "@/app/assets/cover-images/image_9.jpg?url";
|
||||||
|
|
||||||
|
|
@ -41,6 +58,23 @@ export const STATIC_COVER_IMAGES = {
|
||||||
IMAGE_10: CoverImage10,
|
IMAGE_10: CoverImage10,
|
||||||
IMAGE_11: CoverImage11,
|
IMAGE_11: CoverImage11,
|
||||||
IMAGE_12: CoverImage12,
|
IMAGE_12: CoverImage12,
|
||||||
|
IMAGE_13: CoverImage13,
|
||||||
|
IMAGE_14: CoverImage14,
|
||||||
|
IMAGE_15: CoverImage15,
|
||||||
|
IMAGE_16: CoverImage16,
|
||||||
|
IMAGE_17: CoverImage17,
|
||||||
|
IMAGE_18: CoverImage18,
|
||||||
|
IMAGE_19: CoverImage19,
|
||||||
|
IMAGE_20: CoverImage20,
|
||||||
|
IMAGE_21: CoverImage21,
|
||||||
|
IMAGE_22: CoverImage22,
|
||||||
|
IMAGE_23: CoverImage23,
|
||||||
|
IMAGE_24: CoverImage24,
|
||||||
|
IMAGE_25: CoverImage25,
|
||||||
|
IMAGE_26: CoverImage26,
|
||||||
|
IMAGE_27: CoverImage27,
|
||||||
|
IMAGE_28: CoverImage28,
|
||||||
|
IMAGE_29: CoverImage29,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const DEFAULT_COVER_IMAGE_URL = STATIC_COVER_IMAGES.IMAGE_1;
|
export const DEFAULT_COVER_IMAGE_URL = STATIC_COVER_IMAGES.IMAGE_1;
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,8 @@
|
||||||
@layer components {
|
@layer components {
|
||||||
.nodedc-glass-sidebar {
|
.nodedc-glass-sidebar {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035) 0%, rgba(255, 255, 255, 0.018) 100%), rgba(7, 7, 9, 0.84);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.035) 0%, rgba(255, 255, 255, 0.018) 100%),
|
||||||
|
rgba(7, 7, 9, 0.84);
|
||||||
backdrop-filter: blur(40px);
|
backdrop-filter: blur(40px);
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
|
|
@ -230,7 +231,8 @@
|
||||||
|
|
||||||
.nodedc-glass-modal {
|
.nodedc-glass-modal {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(6, 6, 8, 0.9) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(6, 6, 8, 0.9) !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
-webkit-backdrop-filter: blur(42px);
|
-webkit-backdrop-filter: blur(42px);
|
||||||
|
|
@ -242,7 +244,8 @@
|
||||||
|
|
||||||
.nodedc-glass-surface {
|
.nodedc-glass-surface {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.025) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(9, 9, 12, 0.88);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.025) 0%, rgba(255, 255, 255, 0.01) 100%),
|
||||||
|
rgba(9, 9, 12, 0.88);
|
||||||
@apply border border-subtle/70 backdrop-blur-2xl;
|
@apply border border-subtle/70 backdrop-blur-2xl;
|
||||||
-webkit-backdrop-filter: blur(40px);
|
-webkit-backdrop-filter: blur(40px);
|
||||||
backdrop-filter: blur(40px);
|
backdrop-filter: blur(40px);
|
||||||
|
|
@ -253,7 +256,8 @@
|
||||||
|
|
||||||
.nodedc-glass-popup-surface {
|
.nodedc-glass-popup-surface {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(8, 8, 11, 0.9);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(8, 8, 11, 0.9);
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
-webkit-backdrop-filter: blur(44px);
|
-webkit-backdrop-filter: blur(44px);
|
||||||
|
|
@ -265,7 +269,8 @@
|
||||||
|
|
||||||
.nodedc-bottom-dock {
|
.nodedc-bottom-dock {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(7, 7, 10, 0.72) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.008) 100%),
|
||||||
|
rgba(7, 7, 10, 0.72) !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
|
|
@ -347,7 +352,8 @@
|
||||||
|
|
||||||
.nodedc-modal-field {
|
.nodedc-modal-field {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.028);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.028);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
|
|
@ -362,93 +368,8 @@
|
||||||
.nodedc-modal-field:hover,
|
.nodedc-modal-field:hover,
|
||||||
.nodedc-modal-field:focus-within {
|
.nodedc-modal-field:focus-within {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.016) 100%), rgba(255, 255, 255, 0.04);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.016) 100%),
|
||||||
}
|
rgba(255, 255, 255, 0.04);
|
||||||
|
|
||||||
.nodedc-cover-picker {
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.02) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(9, 9, 12, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-cover-picker-tabs {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
padding: 0.25rem;
|
|
||||||
border-radius: 1.1rem;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(255, 255, 255, 0.032);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-cover-picker-tab {
|
|
||||||
display: inline-flex;
|
|
||||||
flex: 1 1 0%;
|
|
||||||
min-height: 2.75rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 0.9rem !important;
|
|
||||||
border: 0 !important;
|
|
||||||
color: var(--text-color-secondary) !important;
|
|
||||||
background: transparent !important;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition:
|
|
||||||
color 160ms ease,
|
|
||||||
background-color 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-cover-picker-tab[data-state="active"] {
|
|
||||||
color: var(--text-color-primary) !important;
|
|
||||||
background: rgba(255, 255, 255, 0.06) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-cover-picker-tile {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.014) 100%), rgba(255, 255, 255, 0.03);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.02),
|
|
||||||
0 12px 28px rgba(0, 0, 0, 0.18);
|
|
||||||
transition:
|
|
||||||
border-color 160ms ease,
|
|
||||||
transform 160ms ease,
|
|
||||||
box-shadow 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-cover-picker-tile:hover {
|
|
||||||
border-color: rgba(255, 255, 255, 0.08);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
|
||||||
0 18px 34px rgba(0, 0, 0, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-cover-picker-tile[data-selected="true"] {
|
|
||||||
border-color: rgba(var(--nodedc-accent-rgb), 0.72);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.38),
|
|
||||||
0 18px 34px rgba(0, 0, 0, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-cover-picker-upload {
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(255, 255, 255, 0.02);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.02),
|
|
||||||
0 16px 34px rgba(0, 0, 0, 0.18);
|
|
||||||
transition:
|
|
||||||
background 160ms ease,
|
|
||||||
border-color 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-cover-picker-upload:hover {
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.014) 100%), rgba(255, 255, 255, 0.032);
|
|
||||||
border-color: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-cover-picker-footer {
|
|
||||||
border-color: rgba(255, 255, 255, 0.06) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-work-item-properties-row {
|
.nodedc-work-item-properties-row {
|
||||||
|
|
@ -480,7 +401,8 @@
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.018) 100%), rgba(255, 255, 255, 0.04) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.018) 100%),
|
||||||
|
rgba(255, 255, 255, 0.04) !important;
|
||||||
color: var(--text-color-primary) !important;
|
color: var(--text-color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -502,7 +424,8 @@
|
||||||
|
|
||||||
.nodedc-modal-input {
|
.nodedc-modal-input {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.028) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.028) !important;
|
||||||
border: 1px solid transparent !important;
|
border: 1px solid transparent !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|
@ -517,7 +440,8 @@
|
||||||
|
|
||||||
.nodedc-modal-editor {
|
.nodedc-modal-editor {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.028) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.028) !important;
|
||||||
border: 1px solid transparent !important;
|
border: 1px solid transparent !important;
|
||||||
border-radius: 1.5rem !important;
|
border-radius: 1.5rem !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -536,7 +460,8 @@
|
||||||
|
|
||||||
.nodedc-dropdown-surface {
|
.nodedc-dropdown-surface {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.025) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(8, 8, 11, 0.9);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.025) 0%, rgba(255, 255, 255, 0.01) 100%),
|
||||||
|
rgba(8, 8, 11, 0.9);
|
||||||
@apply rounded-[1.25rem] px-3 py-3 text-12 outline-none;
|
@apply rounded-[1.25rem] px-3 py-3 text-12 outline-none;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
-webkit-backdrop-filter: blur(44px);
|
-webkit-backdrop-filter: blur(44px);
|
||||||
|
|
@ -786,19 +711,22 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.028) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.028) !important;
|
||||||
color: var(--text-color-secondary) !important;
|
color: var(--text-color-secondary) !important;
|
||||||
padding-inline: 1rem !important;
|
padding-inline: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-modal-chip:hover {
|
.nodedc-modal-chip:hover {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.016) 100%), rgba(255, 255, 255, 0.04) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.038) 0%, rgba(255, 255, 255, 0.016) 100%),
|
||||||
|
rgba(255, 255, 255, 0.04) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-settings-card {
|
.nodedc-settings-card {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.026) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(255, 255, 255, 0.032);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.026) 0%, rgba(255, 255, 255, 0.01) 100%),
|
||||||
|
rgba(255, 255, 255, 0.032);
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|
@ -814,7 +742,8 @@
|
||||||
inset -1px 0 0 rgba(255, 255, 255, 0.06),
|
inset -1px 0 0 rgba(255, 255, 255, 0.06),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.015) !important;
|
inset 0 1px 0 rgba(255, 255, 255, 0.015) !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.008) 100%), rgba(8, 8, 11, 0.9) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.008) 100%),
|
||||||
|
rgba(8, 8, 11, 0.9) !important;
|
||||||
-webkit-backdrop-filter: blur(28px);
|
-webkit-backdrop-filter: blur(28px);
|
||||||
backdrop-filter: blur(28px);
|
backdrop-filter: blur(28px);
|
||||||
}
|
}
|
||||||
|
|
@ -832,13 +761,15 @@
|
||||||
|
|
||||||
.nodedc-settings-sidebar-item:hover {
|
.nodedc-settings-sidebar-item:hover {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.014) 100%), rgba(255, 255, 255, 0.028) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.014) 100%),
|
||||||
|
rgba(255, 255, 255, 0.028) !important;
|
||||||
color: var(--text-color-primary) !important;
|
color: var(--text-color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-settings-sidebar-item[data-active="true"] {
|
.nodedc-settings-sidebar-item[data-active="true"] {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.016) 100%), rgba(255, 255, 255, 0.042) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.016) 100%),
|
||||||
|
rgba(255, 255, 255, 0.042) !important;
|
||||||
color: rgb(var(--nodedc-accent-rgb)) !important;
|
color: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
box-shadow: inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.24) !important;
|
box-shadow: inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.24) !important;
|
||||||
}
|
}
|
||||||
|
|
@ -849,7 +780,8 @@
|
||||||
|
|
||||||
.nodedc-settings-field {
|
.nodedc-settings-field {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.03) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.03) !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|
@ -868,7 +800,8 @@
|
||||||
|
|
||||||
.nodedc-settings-input {
|
.nodedc-settings-input {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.03) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.03) !important;
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
|
@ -893,7 +826,8 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.03) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.03) !important;
|
||||||
color: var(--text-color-primary) !important;
|
color: var(--text-color-primary) !important;
|
||||||
-webkit-backdrop-filter: blur(18px);
|
-webkit-backdrop-filter: blur(18px);
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
|
|
@ -913,7 +847,8 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.03) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.03) !important;
|
||||||
color: var(--text-color-primary) !important;
|
color: var(--text-color-primary) !important;
|
||||||
padding-inline: 1rem !important;
|
padding-inline: 1rem !important;
|
||||||
-webkit-backdrop-filter: blur(18px);
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
|
@ -965,7 +900,8 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.02) 100%), rgba(9, 9, 12, 0.72) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.02) 100%),
|
||||||
|
rgba(9, 9, 12, 0.72) !important;
|
||||||
color: #f5f7fb !important;
|
color: #f5f7fb !important;
|
||||||
padding-inline: 1.05rem !important;
|
padding-inline: 1.05rem !important;
|
||||||
-webkit-backdrop-filter: blur(22px);
|
-webkit-backdrop-filter: blur(22px);
|
||||||
|
|
@ -978,7 +914,8 @@
|
||||||
|
|
||||||
.nodedc-overlay-button:hover {
|
.nodedc-overlay-button:hover {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0.03) 100%), rgba(9, 9, 12, 0.8) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0.03) 100%),
|
||||||
|
rgba(9, 9, 12, 0.8) !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1041,7 +978,8 @@
|
||||||
|
|
||||||
.nodedc-filter-row-shell {
|
.nodedc-filter-row-shell {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(8, 8, 11, 0.84);
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%),
|
||||||
|
rgba(8, 8, 11, 0.84);
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
border-radius: 1.35rem !important;
|
border-radius: 1.35rem !important;
|
||||||
-webkit-backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
|
@ -1147,7 +1085,8 @@
|
||||||
border-radius: 1.9rem !important;
|
border-radius: 1.9rem !important;
|
||||||
padding: 2.2rem !important;
|
padding: 2.2rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.048) 0%, rgba(255, 255, 255, 0.015) 100%), rgba(9, 9, 12, 0.84) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.048) 0%, rgba(255, 255, 255, 0.015) 100%),
|
||||||
|
rgba(9, 9, 12, 0.84) !important;
|
||||||
-webkit-backdrop-filter: blur(40px);
|
-webkit-backdrop-filter: blur(40px);
|
||||||
backdrop-filter: blur(40px);
|
backdrop-filter: blur(40px);
|
||||||
}
|
}
|
||||||
|
|
@ -1177,14 +1116,16 @@
|
||||||
border-radius: 1.15rem !important;
|
border-radius: 1.15rem !important;
|
||||||
min-height: 3rem;
|
min-height: 3rem;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.03) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.03) !important;
|
||||||
-webkit-backdrop-filter: blur(18px);
|
-webkit-backdrop-filter: blur(18px);
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-auth-input-shell[data-error="true"] {
|
.nodedc-auth-input-shell[data-error="true"] {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 82, 82, 0.08) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 82, 82, 0.08) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-auth-input {
|
.nodedc-auth-input {
|
||||||
|
|
@ -1235,7 +1176,8 @@
|
||||||
border-radius: 1.95rem !important;
|
border-radius: 1.95rem !important;
|
||||||
padding: 2.15rem !important;
|
padding: 2.15rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.016) 100%), rgba(9, 9, 12, 0.86) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.016) 100%),
|
||||||
|
rgba(9, 9, 12, 0.86) !important;
|
||||||
-webkit-backdrop-filter: blur(40px);
|
-webkit-backdrop-filter: blur(40px);
|
||||||
backdrop-filter: blur(40px);
|
backdrop-filter: blur(40px);
|
||||||
}
|
}
|
||||||
|
|
@ -1300,7 +1242,8 @@
|
||||||
.nodedc-external-sidebar-shell {
|
.nodedc-external-sidebar-shell {
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(8, 8, 11, 0.86) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.035) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(8, 8, 11, 0.86) !important;
|
||||||
-webkit-backdrop-filter: blur(30px);
|
-webkit-backdrop-filter: blur(30px);
|
||||||
backdrop-filter: blur(30px);
|
backdrop-filter: blur(30px);
|
||||||
}
|
}
|
||||||
|
|
@ -1369,7 +1312,8 @@
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
|
inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
|
||||||
border-radius: 2rem !important;
|
border-radius: 2rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.034) 0%, rgba(255, 255, 255, 0.014) 100%), rgba(255, 255, 255, 0.03) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.034) 0%, rgba(255, 255, 255, 0.014) 100%),
|
||||||
|
rgba(255, 255, 255, 0.03) !important;
|
||||||
-webkit-backdrop-filter: blur(28px);
|
-webkit-backdrop-filter: blur(28px);
|
||||||
backdrop-filter: blur(28px);
|
backdrop-filter: blur(28px);
|
||||||
}
|
}
|
||||||
|
|
@ -1404,7 +1348,8 @@
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
||||||
border-radius: 1.6rem !important;
|
border-radius: 1.6rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.028) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.028) !important;
|
||||||
-webkit-backdrop-filter: blur(22px);
|
-webkit-backdrop-filter: blur(22px);
|
||||||
backdrop-filter: blur(22px);
|
backdrop-filter: blur(22px);
|
||||||
}
|
}
|
||||||
|
|
@ -1509,7 +1454,8 @@
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
border-radius: 1.5rem !important;
|
border-radius: 1.5rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.016) 100%), rgba(8, 8, 11, 0.76) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.016) 100%),
|
||||||
|
rgba(8, 8, 11, 0.76) !important;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 20px 52px rgba(0, 0, 0, 0.22),
|
0 20px 52px rgba(0, 0, 0, 0.22),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.025) !important;
|
inset 0 1px 0 rgba(255, 255, 255, 0.025) !important;
|
||||||
|
|
@ -1537,113 +1483,6 @@
|
||||||
box-shadow 160ms ease;
|
box-shadow 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-home-card {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
isolation: isolate;
|
|
||||||
border: 0 !important;
|
|
||||||
outline: none !important;
|
|
||||||
border-radius: 2rem !important;
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.034) 0%, rgba(255, 255, 255, 0.012) 100%),
|
|
||||||
rgba(255, 255, 255, 0.028) !important;
|
|
||||||
box-shadow:
|
|
||||||
0 18px 40px rgba(0, 0, 0, 0.18),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.028) !important;
|
|
||||||
-webkit-backdrop-filter: blur(28px);
|
|
||||||
backdrop-filter: blur(28px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-card::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top right, rgba(var(--nodedc-accent-rgb), 0.12), transparent 34%),
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.014) 0%, transparent 100%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-card[data-tone="accent"] {
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.042) 0%, rgba(255, 255, 255, 0.016) 100%),
|
|
||||||
rgba(var(--nodedc-accent-rgb), 0.12) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-card[data-tone="accent"]::before {
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top right, rgba(255, 255, 255, 0.16), transparent 30%),
|
|
||||||
radial-gradient(circle at bottom left, rgba(var(--nodedc-accent-rgb), 0.24), transparent 38%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-project-card {
|
|
||||||
height: 14.25rem;
|
|
||||||
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;
|
|
||||||
transition:
|
|
||||||
transform 180ms ease,
|
|
||||||
box-shadow 180ms ease,
|
|
||||||
filter 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);
|
|
||||||
transform: scale(0.965);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-project-card[data-active="false"]:hover {
|
|
||||||
transform: translateY(-0.25rem) scale(0.972);
|
|
||||||
filter: saturate(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-metric-card {
|
|
||||||
border-radius: 1.5rem !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.032) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(0, 0, 0, 0.14);
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-metric-card-accent {
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.042) 0%, rgba(255, 255, 255, 0.016) 100%),
|
|
||||||
rgba(var(--nodedc-accent-rgb), 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-chart-panel {
|
|
||||||
border-radius: 1.75rem !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
||||||
background:
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.032) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(0, 0, 0, 0.14);
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-progress-track {
|
|
||||||
height: 0.55rem;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodedc-home-progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: linear-gradient(90deg, rgba(var(--nodedc-accent-rgb), 0.94) 0%, rgba(255, 255, 255, 0.92) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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%),
|
||||||
|
|
@ -1658,7 +1497,8 @@
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
border-radius: 1.3rem !important;
|
border-radius: 1.3rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.01) 100%), rgba(255, 255, 255, 0.022) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.028) 0%, rgba(255, 255, 255, 0.01) 100%),
|
||||||
|
rgba(255, 255, 255, 0.022) !important;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 14px 32px rgba(0, 0, 0, 0.14),
|
0 14px 32px rgba(0, 0, 0, 0.14),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
||||||
|
|
@ -1705,7 +1545,8 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.028) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.028) !important;
|
||||||
color: var(--text-color-primary) !important;
|
color: var(--text-color-primary) !important;
|
||||||
padding: 0.65rem 0.95rem !important;
|
padding: 0.65rem 0.95rem !important;
|
||||||
}
|
}
|
||||||
|
|
@ -1729,7 +1570,8 @@
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
||||||
border-radius: 1.6rem !important;
|
border-radius: 1.6rem !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.028) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.028) !important;
|
||||||
-webkit-backdrop-filter: blur(22px);
|
-webkit-backdrop-filter: blur(22px);
|
||||||
backdrop-filter: blur(22px);
|
backdrop-filter: blur(22px);
|
||||||
padding: 0.9rem 1rem !important;
|
padding: 0.9rem 1rem !important;
|
||||||
|
|
@ -1893,7 +1735,8 @@
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.018),
|
inset 0 1px 0 rgba(255, 255, 255, 0.018),
|
||||||
0 10px 28px rgba(0, 0, 0, 0.08) !important;
|
0 10px 28px rgba(0, 0, 0, 0.08) !important;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(255, 255, 255, 0.03) !important;
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
|
||||||
|
rgba(255, 255, 255, 0.03) !important;
|
||||||
color: var(--text-color-primary) !important;
|
color: var(--text-color-primary) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const ControlLink = React.forwardRef(function ControlLink(
|
||||||
|
|
||||||
const handleOnClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
const handleOnClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||||
const interactiveTarget = (event.target as HTMLElement | null)?.closest(
|
const interactiveTarget = (event.target as HTMLElement | null)?.closest(
|
||||||
'[data-control-link-ignore="true"],[data-prevent-outside-click],button,[role="button"],input,select,textarea,[contenteditable="true"]'
|
'[data-control-link-ignore="true"],button,[role="button"],input,select,textarea,[contenteditable="true"]'
|
||||||
);
|
);
|
||||||
if (interactiveTarget && interactiveTarget !== event.currentTarget) {
|
if (interactiveTarget && interactiveTarget !== event.currentTarget) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
|
||||||