UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: дизайн и русификация God Mode

This commit is contained in:
DCCONSTRUCTIONS
2026-04-29 00:38:04 +03:00
parent 5d336039ba
commit 7f47b85c36
57 changed files with 1104 additions and 588 deletions
@@ -11,26 +11,35 @@ type Props = {
label?: string;
href?: string;
icon?: React.ReactNode | undefined;
isCurrent?: boolean;
};
export function BreadcrumbLink(props: Props) {
const { href, label, icon } = props;
const { href, label, icon, isCurrent = false } = props;
const content = (
<>
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-16">{icon}</div>}
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
</>
);
return (
<Tooltip tooltipContent={label} position="bottom">
<li className="flex items-center space-x-2" tabIndex={-1}>
<div className="flex flex-wrap items-center gap-2.5">
{href ? (
<Link className="flex items-center gap-1 text-13 font-medium text-tertiary hover:text-primary" href={href}>
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-16">{icon}</div>}
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
</Link>
) : (
<div className="flex cursor-default items-center gap-1 text-13 font-medium text-primary">
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
</div>
)}
</div>
<li className="flex items-center" tabIndex={-1}>
{href && !isCurrent ? (
<Link className="nodedc-admin-breadcrumb-pill flex items-center gap-1.5 text-13 font-medium" href={href}>
{content}
</Link>
) : (
<div
className="nodedc-admin-breadcrumb-pill flex cursor-default items-center gap-1.5 text-13 font-medium"
data-current={isCurrent}
aria-current={isCurrent ? "page" : undefined}
>
{content}
</div>
)}
</li>
</Tooltip>
);
@@ -16,7 +16,7 @@ export function CodeBlock({ children, className, darkerShade }: TProps) {
return (
<span
className={cn(
"rounded-md border border-subtle bg-surface-2 px-0.5 text-11 font-semibold text-tertiary",
"nodedc-code-chip rounded-md border border-subtle bg-surface-2 px-0.5 text-11 font-semibold text-tertiary",
{
"border-subtle bg-layer-1 text-secondary": darkerShade,
},
@@ -45,27 +45,25 @@ export function ConfirmDiscardModal(props: Props) {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
<Dialog.Panel className="nodedc-glass-modal relative transform overflow-hidden rounded-[1.75rem] bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-16 leading-6 font-medium text-tertiary">
You have unsaved changes
Есть несохраненные изменения
</Dialog.Title>
<div className="mt-2">
<p className="text-13 text-placeholder">
Changes you made will be lost if you go back. Do you wish to go back?
</p>
<p className="text-13 text-placeholder">Если уйти назад, текущие правки будут потеряны.</p>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-2 p-4 sm:px-6">
<Button variant="secondary" size="lg" onClick={handleClose}>
Keep editing
Продолжить редактирование
</Button>
<Link href={onDiscardHref} className={getButtonStyling("primary", "base")}>
Go back
Уйти назад
</Link>
</div>
</Dialog.Panel>
@@ -46,7 +46,7 @@ export function ControllerInput(props: Props) {
<Controller
control={control}
name={name}
rules={{ required: required ? `${label} is required.` : false }}
rules={{ required: required ? `Поле "${label}" обязательно.` : false }}
render={({ field: { value, onChange, ref } }) => (
<Input
id={name}
@@ -57,7 +57,7 @@ export function ControllerInput(props: Props) {
ref={ref}
hasError={error}
placeholder={placeholder}
className={cn("w-full rounded-md font-medium", {
className={cn("nodedc-settings-input w-full rounded-md font-medium", {
"pr-10": type === "password",
})}
/>
@@ -27,7 +27,7 @@ export function ControllerSwitch<T extends FieldValues>(props: Props<T>) {
return (
<div className="flex items-center justify-between gap-1">
<h4 className="text-sm text-custom-text-300">Refresh user attributes from {label} during sign in</h4>
<h4 className="text-sm text-custom-text-300">Обновлять атрибуты пользователя из {label} при входе</h4>
<div className="relative">
<Controller
control={control}
@@ -32,18 +32,18 @@ export function CopyField(props: Props) {
<Button
variant="secondary"
size="lg"
className="flex items-center justify-between py-2"
className="nodedc-settings-secondary-button flex w-full items-center justify-between gap-3 py-2 text-left"
onClick={() => {
navigator.clipboard.writeText(url);
setToast({
type: TOAST_TYPE.INFO,
title: "Copied to clipboard",
message: `The ${label} has been successfully copied to your clipboard`,
title: "Скопировано",
message: `${label} скопировано в буфер обмена`,
});
}}
>
<p className="text-13 font-medium">{url}</p>
<CopyIcon width={18} height={18} color="#B9B9B9" />
<p className="min-w-0 truncate text-13 font-medium">{url}</p>
<CopyIcon width={18} height={18} color="#B9B9B9" className="shrink-0" />
</Button>
<div className="text-11 text-tertiary">{description}</div>
</div>
@@ -5,15 +5,15 @@
*/
export const CORE_HEADER_SEGMENT_LABELS: Record<string, string> = {
general: "General",
ai: "Artificial Intelligence",
email: "Email",
authentication: "Authentication",
image: "Image",
general: "Основное",
ai: "Искусственный интеллект",
email: "Почта",
authentication: "Аутентификация",
image: "Изображения",
google: "Google",
github: "GitHub",
gitlab: "GitLab",
gitea: "Gitea",
workspace: "Workspace",
create: "Create",
workspace: "Воркспейсы",
create: "Создание",
};
@@ -4,11 +4,10 @@
* See the LICENSE file for details.
*/
import { Fragment } from "react";
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Menu, Settings } from "lucide-react";
// icons
import { Breadcrumbs } from "@plane/ui";
import { ChevronRight, Menu, Settings } from "lucide-react";
// components
import { BreadcrumbLink } from "../breadcrumb-link";
// hooks
@@ -22,6 +21,7 @@ export const HamburgerToggle = observer(function HamburgerToggle() {
return (
<button
className="group flex size-7 cursor-pointer items-center justify-center rounded-sm bg-layer-1 transition-all hover:bg-layer-1-hover md:hidden"
aria-label="Открыть меню"
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<Menu size={14} className="text-secondary transition-all group-hover:text-primary" />
@@ -56,32 +56,28 @@ export const AdminHeader = observer(function AdminHeader() {
const breadcrumbItems = generateBreadcrumbItems(pathName || "");
return (
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">
<div className="nodedc-admin-header nodedc-glass-modal relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<HamburgerToggle />
{breadcrumbItems.length >= 0 && (
<div>
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
href="/general/"
label="Settings"
icon={<Settings className="h-4 w-4 text-tertiary" />}
/>
}
/>
{breadcrumbItems.map(
(item) =>
item.title && (
<Breadcrumbs.Item
key={item.title}
component={<BreadcrumbLink href={item.href} label={item.title} />}
/>
)
)}
</Breadcrumbs>
</div>
<nav className="min-w-0" aria-label="Навигация God Mode">
<ol className="nodedc-admin-breadcrumbs">
<BreadcrumbLink href="/general/" label="Настройки" icon={<Settings className="h-4 w-4" />} />
{breadcrumbItems.map((item, index) => {
if (!item.title) return null;
const isCurrent = index === breadcrumbItems.length - 1;
return (
<Fragment key={`${item.href}-${item.title}`}>
<li className="nodedc-admin-breadcrumb-separator" aria-hidden="true">
<ChevronRight className="size-4" />
</li>
<BreadcrumbLink href={item.href} label={item.title} isCurrent={isCurrent} />
</Fragment>
);
})}
</ol>
</nav>
)}
</div>
</div>
@@ -24,20 +24,19 @@ export const NewUserPopup = observer(function NewUserPopup() {
if (!isNewUserPopup) return <></>;
return (
<div className="shadow-md absolute right-8 bottom-8 w-96 rounded-lg border border-subtle bg-surface-1 p-6">
<div className="nodedc-glass-modal shadow-md absolute right-8 bottom-8 w-96 rounded-[1.75rem] border border-subtle bg-surface-1 p-6">
<div className="flex gap-4">
<div className="grow">
<div className="text-14 font-semibold">Create workspace</div>
<div className="text-14 font-semibold">Создать воркспейс</div>
<div className="py-2 text-13 font-medium text-tertiary">
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
workspace.
Настройка инстанса завершена. Создайте первое рабочее пространство, чтобы начать работу.
</div>
<div className="flex items-center gap-4 pt-2">
<Link href="/workspace/create" className={getButtonStyling("primary", "lg")}>
Create workspace
Создать воркспейс
</Link>
<Button variant="secondary" size="lg" onClick={toggleNewUserPopup}>
Close
Закрыть
</Button>
</div>
</div>
@@ -46,7 +45,7 @@ export const NewUserPopup = observer(function NewUserPopup() {
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
height={80}
width={80}
alt="Plane icon"
alt="NODE.DC"
/>
</div>
</div>
@@ -10,7 +10,8 @@ type TPageHeader = {
};
export function PageHeader(props: TPageHeader) {
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
const { title = "NODE.DC - глобальное администрирование", description = "Глобальное администрирование NODE.DC" } =
props;
return (
<>
@@ -24,16 +24,16 @@ export const PageWrapper = (props: TPageWrapperProps) => {
return (
<div
className={cn("mx-auto h-full w-full space-y-6 py-4", {
className={cn("nodedc-page mx-auto h-full w-full space-y-6 py-4", {
"max-w-[1000px] md:px-4 2xl:max-w-[1200px]": size === "md",
"px-4 lg:px-12": size === "lg",
})}
>
{customHeader ? (
<div className="mx-4 shrink-0 space-y-1 border-b border-subtle py-4">{customHeader}</div>
<div className="nodedc-page-header mx-4 shrink-0 space-y-1 border-b border-subtle py-4">{customHeader}</div>
) : (
header && (
<div className="mx-4 flex shrink-0 items-center justify-between gap-4 space-y-1 border-b border-subtle py-4">
<div className="nodedc-page-header mx-4 flex shrink-0 items-center justify-between gap-4 space-y-1 border-b border-subtle py-4">
<div className={header.actions ? "flex flex-col gap-1" : "space-y-1"}>
<div className="text-h5-semibold text-primary">{header.title}</div>
<div className="text-body-sm-regular text-secondary">{header.description}</div>
@@ -42,7 +42,7 @@ export const PageWrapper = (props: TPageWrapperProps) => {
</div>
)
)}
<div className="vertical-scrollbar scrollbar-sm flex-grow overflow-hidden overflow-y-scroll px-4 pb-4">
<div className="nodedc-page-body vertical-scrollbar scrollbar-sm flex-grow overflow-hidden overflow-y-scroll px-4 pb-4">
{children}
</div>
</div>