UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: внешний контур, auth empty state и HDESIGN update

This commit is contained in:
DCCONSTRUCTIONS
2026-04-20 13:30:04 +03:00
parent b1f980bc7f
commit 9553c81752
16 changed files with 551 additions and 294 deletions
@@ -74,8 +74,8 @@ export function AuthHeaderBase(props: TAuthHeaderBase) {
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6 px-2 py-1">
<Link href="/">
<PlaneLockup
height={54}
width={258}
height={84}
width={402}
className="nodedc-auth-logo-lockup text-primary transition-opacity hover:opacity-90"
/>
</Link>
@@ -6,6 +6,7 @@
import type { ReactNode } from "react";
import { useRef, useState } from "react";
import { createPortal } from "react-dom";
import { usePopper } from "react-popper";
import { SignalHigh } from "lucide-react";
import { Combobox } from "@headlessui/react";
@@ -328,6 +329,7 @@ export function PriorityDropdown(props: Props) {
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
strategy: "fixed",
placement: placement ?? "bottom-start",
modifiers: [
{
@@ -378,7 +380,7 @@ export function PriorityDropdown(props: Props) {
<button
ref={setReferenceElement}
type="button"
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
className={cn("clickable block h-full w-full border-0 bg-transparent shadow-none outline-none", buttonContainerClassName)}
onClick={handleOnClick}
disabled={disabled}
tabIndex={tabIndex}
@@ -390,7 +392,7 @@ export function PriorityDropdown(props: Props) {
ref={setReferenceElement}
type="button"
className={cn(
"clickable block h-full max-w-full outline-none",
"clickable block h-full max-w-full border-0 bg-transparent shadow-none outline-none",
{
"cursor-not-allowed text-secondary": disabled,
"cursor-pointer": !disabled,
@@ -428,56 +430,60 @@ export function PriorityDropdown(props: Props) {
button={comboButton}
renderByDefault={renderByDefault}
>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<div
className="nodedc-dropdown-surface my-1 w-52"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="nodedc-dropdown-search">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-0 text-12 text-secondary placeholder:text-placeholder outline-none focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("search")}
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/>
{isOpen &&
typeof document !== "undefined" &&
createPortal(
<Combobox.Options className="fixed z-[760]" static>
<div
data-prevent-outside-click
className="nodedc-dropdown-surface nodedc-external-popup-anchor my-1 w-56"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="nodedc-dropdown-search">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-0 text-12 text-secondary placeholder:text-placeholder outline-none focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("search")}
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-56 space-y-1 overflow-y-auto">
{filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
cn(
`nodedc-dropdown-option cursor-pointer ${
active ? "bg-white/6" : ""
} ${selected ? "text-primary" : "text-secondary"}`
)
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("no_matching_results")}</p>
)}
</div>
</div>
<div className="mt-2 max-h-56 space-y-1 overflow-y-auto">
{filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
cn(
`nodedc-dropdown-option cursor-pointer ${
active ? "bg-white/6" : ""
} ${selected ? "text-primary" : "text-secondary"}`
)
}
>
{({ selected }) => (
<>
<span className="flex-grow truncate">{option.content}</span>
{selected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
</>
)}
</Combobox.Option>
))
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("no_matching_results")}</p>
)}
</div>
</div>
</Combobox.Options>
)}
</Combobox.Options>,
document.body
)}
</ComboDropDown>
);
}
@@ -55,9 +55,11 @@ export const IssueAttachmentUpload = observer(function IssueAttachmentUpload(pro
return (
<div
{...getRootProps()}
className={`flex h-[60px] items-center justify-center rounded-md border-2 border-dashed bg-accent-primary/5 px-4 text-11 text-accent-primary ${
isDragActive ? "border-accent-strong bg-accent-primary/10" : "border-subtle"
} ${isDragReject ? "bg-danger-subtle" : ""} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
data-drag-active={isDragActive ? "true" : "false"}
data-drag-reject={isDragReject ? "true" : "false"}
className={`nodedc-attachment-upload flex items-center justify-center px-4 text-11 ${
disabled ? "cursor-not-allowed" : "cursor-pointer"
}`}
>
<input {...getInputProps()} />
<span className="flex items-center gap-2">
@@ -25,14 +25,13 @@ export const ProjectViewEmptyState = observer(function ProjectViewEmptyState() {
);
return (
// TODO: Add translation
<EmptyStateDetailed
assetKey="work-item"
title="View work items will appear here"
description="Work items help you track individual pieces of work. With work items, keep track of what's going on, who is working on it, and what's done."
title="Начните с вашего первого рабочего элемента."
description="Рабочие элементы — это строительные блоки вашего проекта: назначайте ответственных, устанавливайте приоритеты и отслеживайте прогресс."
actions={[
{
label: "New work item",
label: "Создайте свой первый рабочий элемент",
onClick: () => {
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW);
},
@@ -5,6 +5,7 @@
*/
import React, { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import type { Placement } from "@popperjs/core";
import { observer } from "mobx-react";
import { usePopper } from "react-popper";
@@ -72,6 +73,7 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
const { isMobile } = usePlatformOS();
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
strategy: "fixed",
placement: placement ?? "bottom-start",
});
// derived values
@@ -169,7 +171,10 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
<button
type="button"
ref={setReferenceElement}
className={cn("flex h-full cursor-pointer items-center gap-2 text-11", buttonContainerClassName)}
className={cn(
"flex h-full cursor-pointer items-center gap-2 border-0 bg-transparent text-11 shadow-none outline-none",
buttonContainerClassName
)}
onClick={handleOnClick}
>
{label ? (
@@ -195,133 +200,137 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
)}
</button>
{isDropdownOpen && (
<Combobox.Options className="fixed z-[120]" static>
<div
className="nodedc-dropdown-surface my-1 w-64 min-w-[16rem]"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="nodedc-dropdown-search">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-0 text-12 text-secondary placeholder:text-placeholder outline-none focus:outline-none"
onChange={(event) => setQuery(event.target.value)}
placeholder={t("search")}
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-56 space-y-1 overflow-y-auto">
{labelsList && filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((label) => {
const children = labelsList?.filter((l) => l.parent === label.id);
{isDropdownOpen &&
typeof document !== "undefined" &&
createPortal(
<Combobox.Options className="fixed z-[760]" static>
<div
data-prevent-outside-click
className="nodedc-dropdown-surface nodedc-external-popup-anchor my-1 w-64 min-w-[16rem]"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="nodedc-dropdown-search">
<SearchIcon className="h-3.5 w-3.5 text-placeholder" strokeWidth={1.5} />
<Combobox.Input
as="input"
ref={inputRef}
className="w-full bg-transparent py-0 text-12 text-secondary placeholder:text-placeholder outline-none focus:outline-none"
onChange={(event) => setQuery(event.target.value)}
placeholder={t("search")}
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-56 space-y-1 overflow-y-auto">
{labelsList && filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((label) => {
const children = labelsList?.filter((l) => l.parent === label.id);
if (children.length === 0) {
if (!label.parent)
return (
<Combobox.Option
key={label.id}
className={({ active }) =>
cn(
"nodedc-dropdown-option cursor-pointer",
active ? "bg-white/6" : "",
"text-secondary"
)
}
value={label.id}
>
{({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded-[0.9rem]">
<div className="flex items-center justify-start gap-2 truncate">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color,
}}
/>
<span className="truncate">{label.name}</span>
</div>
<div className="flex shrink-0 items-center justify-center rounded-[0.9rem] p-1">
<CheckIcon className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
</div>
</div>
)}
</Combobox.Option>
);
} else
return (
<div key={label.id} className="overflow-hidden rounded-[1rem] bg-white/[0.02]">
<div className="flex items-center gap-2 truncate p-2 text-primary select-none">
<Component className="h-3 w-3" /> {label.name}
</div>
<div>
{children.map((child) => (
<Combobox.Option
key={child.id}
className={({ active }) =>
cn(
"nodedc-dropdown-option min-w-[14rem] cursor-pointer",
active ? "bg-white/6" : "",
"text-secondary"
)
}
value={child.id}
>
{({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded-[0.9rem]">
<div className="flex items-center justify-start gap-2">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: child?.color,
}}
/>
<span>{child.name}</span>
</div>
<div className="flex items-center justify-center rounded-[0.9rem] p-1">
<CheckIcon className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
</div>
if (children.length === 0) {
if (!label.parent)
return (
<Combobox.Option
key={label.id}
className={({ active }) =>
cn(
"nodedc-dropdown-option cursor-pointer",
active ? "bg-white/6" : "",
"text-secondary"
)
}
value={label.id}
>
{({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded-[0.9rem]">
<div className="flex items-center justify-start gap-2 truncate">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color,
}}
/>
<span className="truncate">{label.name}</span>
</div>
)}
</Combobox.Option>
))}
<div className="flex shrink-0 items-center justify-center rounded-[0.9rem] p-1">
<CheckIcon className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
</div>
</div>
)}
</Combobox.Option>
);
} else
return (
<div key={label.id} className="overflow-hidden rounded-[1rem] bg-white/[0.02]">
<div className="flex items-center gap-2 truncate p-2 text-primary select-none">
<Component className="h-3 w-3" /> {label.name}
</div>
<div>
{children.map((child) => (
<Combobox.Option
key={child.id}
className={({ active }) =>
cn(
"nodedc-dropdown-option min-w-[14rem] cursor-pointer",
active ? "bg-white/6" : "",
"text-secondary"
)
}
value={child.id}
>
{({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded-[0.9rem]">
<div className="flex items-center justify-start gap-2">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: child?.color,
}}
/>
<span>{child.name}</span>
</div>
<div className="flex items-center justify-center rounded-[0.9rem] p-1">
<CheckIcon className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
</div>
</div>
)}
</Combobox.Option>
))}
</div>
</div>
</div>
);
})
) : submitting ? (
<Loader className="h-3.5 w-3.5 animate-spin" />
) : createLabelEnabled ? (
<p
onClick={() => {
if (!query.length) return;
handleAddLabel(query);
}}
className={`rounded-[0.9rem] px-2 py-2 text-left text-secondary ${query.length ? "cursor-pointer hover:bg-white/6" : "cursor-default"}`}
>
{query.length ? (
<>
+ {t("label.create.type")} <span className="text-primary">&quot;{query}&quot;</span>
</>
) : (
t("label.create.type")
)}
</p>
);
})
) : submitting ? (
<Loader className="h-3.5 w-3.5 animate-spin" />
) : createLabelEnabled ? (
<p
onClick={() => {
if (!query.length) return;
handleAddLabel(query);
}}
className={`rounded-[0.9rem] px-2 py-2 text-left text-secondary ${query.length ? "cursor-pointer hover:bg-white/6" : "cursor-default"}`}
>
{query.length ? (
<>
+ {t("label.create.type")} <span className="text-primary">&quot;{query}&quot;</span>
</>
) : (
t("label.create.type")
)}
</p>
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("no_matching_results")}</p>
)
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("no_matching_results")}</p>
)
) : (
<p className="px-1.5 py-1 text-placeholder italic">{t("loading")}</p>
)}
<p className="px-1.5 py-1 text-placeholder italic">{t("loading")}</p>
)}
</div>
</div>
</div>
</Combobox.Options>
)}
</Combobox.Options>,
document.body
)}
</Combobox>
);
});
@@ -178,6 +178,7 @@ export class ProjectExternalContoursStore implements IProjectExternalContoursSto
};
handleCurrentTab = async (workspaceSlug: string, projectId: string, tab: TInboxIssueCurrentTab) => {
this.currentProjectId = projectId;
this.requestIds = [];
this.requests = {};
this.error = undefined;