UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: быстрый выбор проектов на home

This commit is contained in:
DCCONSTRUCTIONS 2026-04-29 22:37:30 +03:00
parent d9a63f5f74
commit d28f83fe5e
2 changed files with 113 additions and 87 deletions

View File

@ -43,11 +43,16 @@ export function HomeProjectStack(props: HomeProjectStackProps) {
} = props;
const activeProject = projects.find((project: THomeProjectData) => project.id === selectedProjectId);
const orderedProjects = activeProject
const selectedProject = activeProject ?? projects[0];
const selectedProjectIndex = Math.max(
projects.findIndex((project: THomeProjectData) => project.id === selectedProject?.id),
0
);
const stackProjects = activeProject
? [activeProject, ...projects.filter((project: THomeProjectData) => project.id !== activeProject.id)]
: projects;
const visibleProjects = orderedProjects.slice(0, STACK_VISIBLE_LIMIT);
const visibleProjects = stackProjects.slice(0, STACK_VISIBLE_LIMIT);
const activityCountByProject = (recents ?? []).reduce<Record<string, number>>((acc, activity) => {
const projectId = getActivityProjectId(activity);
if (!projectId) return acc;
@ -56,8 +61,6 @@ export function HomeProjectStack(props: HomeProjectStackProps) {
return acc;
}, {});
const selectedProject =
orderedProjects.find((project: THomeProjectData) => project.id === selectedProjectId) ?? orderedProjects[0];
const stackHeight =
visibleProjects.length > 0 ? ACTIVE_CARD_HEIGHT + (visibleProjects.length - 1) * STACK_OFFSET : 228;
const isHorizontal = orientation === "horizontal";
@ -191,19 +194,7 @@ export function HomeProjectStack(props: HomeProjectStackProps) {
</button>
</div>
{isHorizontal ? (
<div className="nodedc-home-project-deck-scroller">
<div className="nodedc-home-project-deck-row flex items-start px-1 py-2">
{visibleProjects.map((project: THomeProjectData, index: number) => renderProjectCard(project, index, true))}
</div>
</div>
) : (
<div className="relative" style={{ height: `${stackHeight}px` }}>
{visibleProjects.map((project: THomeProjectData, index: number) => renderProjectCard(project, index, false))}
</div>
)}
<div className="nodedc-home-project-quick-section mt-4 rounded-[24px] bg-black/10 p-4 xl:mt-auto">
<div className="nodedc-home-project-quick-section mb-4 rounded-[24px] 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>
@ -216,9 +207,11 @@ export function HomeProjectStack(props: HomeProjectStackProps) {
</div>
<div className="nodedc-home-project-quick-list">
{orderedProjects.map((project: THomeProjectData) => {
{projects.map((project: THomeProjectData, index: number) => {
const analytics = analyticsMap[project.id];
const isActive = project.id === selectedProject?.id;
const completionRate = getCompletionRate(analytics);
const distanceFromActive = Math.min(Math.abs(index - selectedProjectIndex), 2);
return (
<button
@ -226,6 +219,7 @@ export function HomeProjectStack(props: HomeProjectStackProps) {
type="button"
className="nodedc-home-project-quick-button"
data-active={isActive}
data-distance={distanceFromActive}
aria-label={
isActive ? `Открыть рабочую область проекта ${project.name}` : `Выбрать проект ${project.name}`
}
@ -240,45 +234,54 @@ export function HomeProjectStack(props: HomeProjectStackProps) {
>
<span className="nodedc-home-project-quick-main">
<span className="nodedc-home-project-quick-logo">
<Logo logo={project.logo_props} size={14} />
<FolderOpenDot className="size-4" />
</span>
<span className="truncate">{project.identifier}</span>
</span>
<span className="nodedc-home-project-quick-metric">
<span className="nodedc-home-project-quick-dot" aria-hidden="true" />
<span>{getCompletionRate(analytics)}%</span>
<span className="nodedc-home-project-quick-name truncate">{project.name}</span>
</span>
<span className="nodedc-home-project-quick-rate">{completionRate}%</span>
</button>
);
})}
</div>
</div>
{selectedProject && (
<div className="nodedc-home-project-focus-grid mt-4 grid grid-cols-2 gap-3 md:grid-cols-3">
<div className="nodedc-home-project-focus-item px-3 py-2">
<div className="text-[11px] tracking-[0.18em] text-placeholder uppercase">Фокус</div>
<div className="nodedc-home-project-focus-value mt-1 text-13 font-semibold text-primary">
{selectedProject.identifier}
</div>
</div>
<div className="nodedc-home-project-focus-item 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="nodedc-home-project-focus-value mt-1 text-13 font-semibold text-primary">
{analyticsMap[selectedProject.id]?.total_members ?? 0}
</div>
</div>
<div className="nodedc-home-project-focus-item px-3 py-2">
<div className="text-[11px] tracking-[0.18em] text-placeholder uppercase">Контур</div>
<div className="nodedc-home-project-focus-value mt-1 text-13 font-semibold text-primary">
{activityCountByProject[selectedProject.id] ?? 0} касаний
</div>
{isHorizontal ? (
<div className="nodedc-home-project-deck-scroller">
<div className="nodedc-home-project-deck-row flex items-start px-1 py-2">
{visibleProjects.map((project: THomeProjectData, index: number) => renderProjectCard(project, index, true))}
</div>
</div>
) : (
<div className="relative" style={{ height: `${stackHeight}px` }}>
{visibleProjects.map((project: THomeProjectData, index: number) => renderProjectCard(project, index, false))}
</div>
)}
{selectedProject && (
<div className="nodedc-home-project-focus-grid mt-4 grid grid-cols-2 gap-3 md:grid-cols-3">
<div className="nodedc-home-project-focus-item px-3 py-2">
<div className="text-[11px] tracking-[0.18em] text-placeholder uppercase">Фокус</div>
<div className="nodedc-home-project-focus-value mt-1 text-13 font-semibold text-primary">
{selectedProject.identifier}
</div>
</div>
)}
</div>
<div className="nodedc-home-project-focus-item 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="nodedc-home-project-focus-value mt-1 text-13 font-semibold text-primary">
{analyticsMap[selectedProject.id]?.total_members ?? 0}
</div>
</div>
<div className="nodedc-home-project-focus-item px-3 py-2">
<div className="text-[11px] tracking-[0.18em] text-placeholder uppercase">Контур</div>
<div className="nodedc-home-project-focus-value mt-1 text-13 font-semibold text-primary">
{activityCountByProject[selectedProject.id] ?? 0} касаний
</div>
</div>
</div>
)}
</section>
);
}

View File

@ -3168,89 +3168,112 @@
.nodedc-home-project-quick-list {
display: flex;
width: calc(100% + 2rem);
margin-inline: -1rem;
width: calc(100% + 1rem);
margin-inline: -0.5rem;
flex-direction: column;
gap: 0;
overflow: hidden;
border-radius: 1.25rem;
background: rgba(8, 8, 10, 0.62);
gap: 0.22rem;
overflow: visible;
}
.nodedc-home-project-quick-button {
position: relative;
display: flex;
width: 100%;
min-height: 3.22rem;
min-height: 3.18rem;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
gap: 0.86rem;
border: 0 !important;
outline: none !important;
border-radius: 0 !important;
background: rgba(255, 255, 255, 0.028) !important;
border-radius: 999px !important;
background: rgba(255, 255, 255, 0.04) !important;
padding: 0.7rem 0.9rem;
padding-left: 3.72rem;
color: var(--text-color-primary);
font-size: 0.95rem;
font-weight: 520;
text-align: left;
opacity: 0.42;
transition:
background 160ms ease,
color 160ms ease,
opacity 160ms ease,
transform 160ms ease,
box-shadow 160ms ease;
}
.nodedc-home-project-quick-button + .nodedc-home-project-quick-button {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045);
}
.nodedc-home-project-quick-button:first-child {
border-top-left-radius: 1.25rem !important;
border-top-right-radius: 1.25rem !important;
}
.nodedc-home-project-quick-button:last-child {
border-bottom-right-radius: 1.25rem !important;
border-bottom-left-radius: 1.25rem !important;
}
.nodedc-home-project-quick-button:hover {
background: rgba(255, 255, 255, 0.08) !important;
background: rgba(255, 255, 255, 0.085) !important;
color: var(--text-color-primary);
opacity: 0.92;
transform: translateY(-1px);
}
.nodedc-home-project-quick-button[data-active="true"] {
background: rgba(255, 255, 255, 0.13) !important;
background: rgba(255, 255, 255, 0.115) !important;
color: var(--text-color-primary);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.09);
opacity: 0.82;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.048),
0 10px 22px rgba(0, 0, 0, 0.12);
}
.nodedc-home-project-quick-button[data-distance="1"] {
opacity: 0.6;
}
.nodedc-home-project-quick-button[data-distance="2"] {
opacity: 0.4;
}
.nodedc-home-project-quick-main {
display: flex;
min-width: 0;
align-items: center;
gap: 0.62rem;
}
.nodedc-home-project-quick-logo {
position: absolute;
top: 50%;
left: 0.12rem;
transform: translateY(-50%);
display: grid;
width: 1.7rem;
min-width: 1.7rem;
height: 1.7rem;
width: 2.92rem;
min-width: 2.92rem;
height: 2.92rem;
place-items: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
background: rgba(255, 255, 255, 0.07);
color: rgba(255, 255, 255, 0.74);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.05),
0 8px 20px rgba(0, 0, 0, 0.16);
}
.nodedc-home-project-quick-button[data-active="true"] .nodedc-home-project-quick-logo {
background: rgba(var(--nodedc-card-active-rgb), 0.92);
color: #0a0a0b;
background: rgb(var(--nodedc-card-active-rgb));
color: rgb(var(--nodedc-on-card-active-rgb));
}
.nodedc-home-project-quick-name {
min-width: 0;
color: var(--text-color-primary);
font-size: 0.91rem;
font-weight: 570;
}
.nodedc-home-project-quick-rate {
min-width: max-content;
color: rgba(255, 255, 255, 0.62);
font-size: 0.76rem;
font-weight: 650;
line-height: 1;
}
.nodedc-home-project-quick-button[data-active="true"] .nodedc-home-project-quick-rate {
color: rgba(255, 255, 255, 0.78);
}
.nodedc-home-project-quick-metric {
display: inline-flex;
display: none;
min-width: max-content;
align-items: center;
gap: 0.38rem;
@ -3260,7 +3283,7 @@
}
.nodedc-home-project-quick-dot {
display: block;
display: none;
width: 0.48rem;
height: 0.48rem;
border-radius: 999px;