From 64527ebd5bbc0c2f1109f754f2e6ba7243d60568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Fri, 14 Aug 2026 11:35:49 +0500 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=D0=BD=D0=B0=D0=B2=D0=B8=D0=B3?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BD=D0=B0=20ui-kit=20SIdebar=20+?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=B1=D0=B8=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20dra?= =?UTF-8?q?wer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/RequestPortal.module.scss | 17 +- .../components/RequestPortal.tsx | 31 +++- .../components/SideNav/SideNav.module.scss | 154 ------------------ .../components/SideNav/SideNav.tsx | 87 ---------- 4 files changed, 34 insertions(+), 255 deletions(-) delete mode 100644 src/webparts/requestPortal/components/SideNav/SideNav.module.scss delete mode 100644 src/webparts/requestPortal/components/SideNav/SideNav.tsx diff --git a/src/webparts/requestPortal/components/RequestPortal.module.scss b/src/webparts/requestPortal/components/RequestPortal.module.scss index 73d0694..0b1549c 100644 --- a/src/webparts/requestPortal/components/RequestPortal.module.scss +++ b/src/webparts/requestPortal/components/RequestPortal.module.scss @@ -81,15 +81,22 @@ display: none; } -// На узких экранах рейл уходит наверх. -@media (max-width: 900px) { +// Мобильная кнопка-бургер (открывает выезжающий Sidebar). На десктопе скрыта. +.mobileNav { + display: none; + margin-bottom: $sp-4; +} + +// < 768px: рейл превращается в выезжающий drawer (см. @tnsu/ui-kit Sidebar). Он +// position:fixed и выходит из потока — колонка .side схлопывается сама, main +// занимает всю ширину. Показываем бургер в шапке контента. +@media (max-width: 768px) { .layout { - flex-direction: column; min-height: 0; } - .side { - width: 100%; + .mobileNav { + display: block; } } diff --git a/src/webparts/requestPortal/components/RequestPortal.tsx b/src/webparts/requestPortal/components/RequestPortal.tsx index f99288d..35b3144 100644 --- a/src/webparts/requestPortal/components/RequestPortal.tsx +++ b/src/webparts/requestPortal/components/RequestPortal.tsx @@ -2,7 +2,8 @@ import * as React from 'react'; import styles from './RequestPortal.module.scss'; import '@tnsu/ui-kit/styles.css'; import type { IRequestPortalProps } from './IRequestPortalProps'; -import { IconSprite } from '@tnsu/ui-kit'; +import { IconSprite, Sidebar, SidebarToggle } from '@tnsu/ui-kit'; +import type { ISidebarItem } from '@tnsu/ui-kit'; import { ConfirmHost } from '../shared/ui'; import { navigate, @@ -20,7 +21,6 @@ import { } from '../shared/routing'; import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents'; import { confirmDiscardFormChanges, markFormClean, markFormDirty } from '../shared/unsavedChanges'; -import { SideNav } from './SideNav/SideNav'; import { Drawer } from './Drawer/Drawer'; import { CategoryTabs } from './CategoryTabs/CategoryTabs'; import { CATEGORIES, DEFAULT_CATEGORY_ID } from '../data/categories'; @@ -59,6 +59,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context const route = useRoute(); const [navCollapsed, setNavCollapsed] = React.useState(false); + const [mobileNavOpen, setMobileNavOpen] = React.useState(false); const [navCounts, setNavCounts] = React.useState(undefined); const [activeCategoryId, setActiveCategoryId] = React.useState(() => { @@ -250,26 +251,38 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context }).catch(() => undefined); }; + // Разделы бокового меню для пакетного Sidebar (счётчики — из navCounts). + const navItems: ISidebarItem[] = [ + { id: 'new', title: 'Новая заявка', icon: 'plus' }, + { id: 'approvals', title: 'На согласовании', icon: 'user-check', count: navCounts?.approvals }, + { id: 'execution', title: 'На исполнении', icon: 'helmet', count: navCounts?.execution }, + { id: 'rework', title: 'Доработка', icon: 'edit', count: navCounts?.rework }, + { id: 'own', title: 'Мои заявки', icon: 'clipboard' }, + { id: 'search', title: 'Поиск', icon: 'search' }, + ]; + return (
- goto(id as NavSection)} + brand="Портал заявок" collapsed={navCollapsed} onToggleCollapsed={() => setNavCollapsed((v) => !v)} + mobileOpen={mobileNavOpen} + onMobileClose={() => setMobileNavOpen(false)} />
+
+ setMobileNavOpen(true)} /> +
{/* Раздел «Новая заявка» держим СМОНТИРОВАННЫМ и просто прячем стилем, когда открыта другая вкладка. Иначе React размонтировал бы форму, и всё введённое пропадало бы при переключении на «Мои заявки» и diff --git a/src/webparts/requestPortal/components/SideNav/SideNav.module.scss b/src/webparts/requestPortal/components/SideNav/SideNav.module.scss deleted file mode 100644 index 4b4a3e0..0000000 --- a/src/webparts/requestPortal/components/SideNav/SideNav.module.scss +++ /dev/null @@ -1,154 +0,0 @@ -@import '../../../../styles/design-tokens'; - -// Тёмный боковой рейл портала на всю высоту (фирменный navy-градиент, золотой -// маркер активного пункта). Не «плавает»: примыкает к контенту, тянется по высоте. -.nav { - display: flex; - flex-direction: column; - width: 248px; - padding: $sp-4 0 $sp-3; - background: $grad-header; - box-shadow: inset -1px 0 0 rgba(255, 255, 255, .06); - transition: width .2s; -} - -.navCollapsed { - width: 64px; -} - -// ---- Шапка рейла: бренд + сворачивание ---- -.head { - display: flex; - align-items: center; - justify-content: space-between; - gap: $sp-2; - padding: 0 $sp-4 $sp-4; - border-bottom: 1px solid rgba(255, 255, 255, .08); -} - -.navCollapsed .head { - flex-direction: column; - gap: $sp-3; - padding: 0 0 $sp-3; -} - -.brand { - display: flex; - align-items: center; - gap: $sp-2; - min-width: 0; -} - -.brandText { - font-family: $font-serif; - font-size: 15px; - font-weight: 600; - color: $white; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.toggle { - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - padding: 0; - border: 1px solid rgba(255, 255, 255, .18); - border-radius: $r-sm; - background: transparent; - color: rgba(255, 255, 255, .6); - cursor: pointer; - transition: .16s; - - &:hover { - border-color: $gold-400; - color: $gold-300; - } -} - -// ---- Пункты разделов ---- -.items { - display: flex; - flex-direction: column; - gap: 2px; - margin-top: $sp-4; -} - -.navCollapsed .items { - margin-top: $sp-3; -} - -.item { - position: relative; - display: flex; - align-items: center; - gap: $sp-3; - width: 100%; - padding: 12px 18px; - border: 0; - border-left: 3px solid transparent; - background: transparent; - color: rgba(255, 255, 255, .62); - font-family: $font-sans; - font-size: 13.5px; - font-weight: 500; - text-align: left; - white-space: nowrap; - cursor: pointer; - transition: color .18s, background .18s, border-color .18s; - - &:hover { - color: rgba(255, 255, 255, .92); - background: rgba(255, 255, 255, .04); - } -} - -.navCollapsed .item { - justify-content: center; - padding: 12px 0; -} - -.itemActive { - color: $white; - background: rgba(255, 255, 255, .07); - border-left-color: $gold-500; - - .itemIcon { - color: $gold-400; - } -} - -.itemIcon { - flex-shrink: 0; - display: inline-flex; - color: inherit; -} - -.itemTitle { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -} - -.badge { - flex-shrink: 0; - min-width: 18px; - padding: 1px 6px; - border-radius: $r-full; - background: $gold-500; - color: $navy-950; - font-size: $fs-overline; - font-weight: 700; - text-align: center; -} - -.navCollapsed .badge { - position: absolute; - top: 6px; - right: 8px; -} diff --git a/src/webparts/requestPortal/components/SideNav/SideNav.tsx b/src/webparts/requestPortal/components/SideNav/SideNav.tsx deleted file mode 100644 index 581b343..0000000 --- a/src/webparts/requestPortal/components/SideNav/SideNav.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import * as React from 'react'; -import styles from './SideNav.module.scss'; -import { Icon, IconName } from '@tnsu/ui-kit'; -import { NavSection } from '../../shared/routing'; - -interface INavItem { - id: NavSection; - title: string; - icon: IconName; -} - -// Порядок неслучаен: сверху действие (создать), затем три очереди — то, что -// ждёт пользователя прямо сейчас, — и только потом архив и поиск. -const ITEMS: INavItem[] = [ - { id: 'new', title: 'Новая заявка', icon: 'plus' }, - { id: 'approvals', title: 'На согласовании', icon: 'user-check' }, - { id: 'execution', title: 'На исполнении', icon: 'helmet' }, - { id: 'rework', title: 'Доработка', icon: 'edit' }, - { id: 'own', title: 'Мои заявки', icon: 'clipboard' }, - { id: 'search', title: 'Поиск', icon: 'search' }, -]; - -interface ISideNavProps { - activeId: NavSection; - onChange: (id: NavSection) => void; - /** Счётчики очередей: сколько заявок ждёт пользователя в каждом разделе. */ - counts?: Partial>; - collapsed: boolean; - onToggleCollapsed: () => void; -} - -// Тёмный боковой рейл портала на всю высоту: бренд сверху, разделы снизу. -// Сворачивается до узкой полосы с одними иконками. -export const SideNav: React.FC = ({ - activeId, - onChange, - counts, - collapsed, - onToggleCollapsed, -}) => { - const navClasses = [styles.nav, collapsed ? styles.navCollapsed : ''].filter(Boolean).join(' '); - - return ( - - ); -}; From a937e33f2e6e57f89fa00ebeb3d69067f8a785e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Fri, 14 Aug 2026 12:07:09 +0500 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20StatusBadge=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=85=20ui-kit=20Badge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 8 +-- package.json | 2 +- .../approval/ui/StatusBadge.module.scss | 63 ------------------- .../shared/approval/ui/StatusBadge.tsx | 47 ++++++-------- 4 files changed, 25 insertions(+), 95 deletions(-) delete mode 100644 src/webparts/requestPortal/shared/approval/ui/StatusBadge.module.scss diff --git a/package-lock.json b/package-lock.json index 0242ff2..a984264 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@microsoft/sp-webpart-base": "1.22.1", "@pnp/sp": "^4.20.0", "@pnp/spfx-controls-react": "^3.25.0", - "@tnsu/ui-kit": "^0.2.0", + "@tnsu/ui-kit": "^0.3.0", "react": "17.0.1", "react-dom": "17.0.1", "tslib": "2.3.1" @@ -9870,9 +9870,9 @@ "license": "0BSD" }, "node_modules/@tnsu/ui-kit": { - "version": "0.2.0", - "resolved": "http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/%40tnsu%2Fui-kit/-/0.2.0/ui-kit-0.2.0.tgz", - "integrity": "sha512-8vQsA91lSbmtlqROlIYkgSH3cNEH5DCVI5vM4mks2RWwvSpYet0zfe2B3wfBcnb+cAWrbLGfCwe81KRDcJ/k+Q==", + "version": "0.3.0", + "resolved": "http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/%40tnsu%2Fui-kit/-/0.3.0/ui-kit-0.3.0.tgz", + "integrity": "sha512-V9pTS/rBmU9KInczI8jJ/KHT96hfX0OlnjIJ2NZ/iEQTOE93I9xMfmgpj83PSRrI6/xzyybu+8k/qVO6LP926w==", "license": "ISC", "peerDependencies": { "@fluentui/react": "^8.106.4", diff --git a/package.json b/package.json index f9bc860..50ca8b8 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@microsoft/sp-webpart-base": "1.22.1", "@pnp/sp": "^4.20.0", "@pnp/spfx-controls-react": "^3.25.0", - "@tnsu/ui-kit": "^0.2.0", + "@tnsu/ui-kit": "^0.3.0", "react": "17.0.1", "react-dom": "17.0.1", "tslib": "2.3.1" diff --git a/src/webparts/requestPortal/shared/approval/ui/StatusBadge.module.scss b/src/webparts/requestPortal/shared/approval/ui/StatusBadge.module.scss deleted file mode 100644 index 3753365..0000000 --- a/src/webparts/requestPortal/shared/approval/ui/StatusBadge.module.scss +++ /dev/null @@ -1,63 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.badge { - display: inline-flex; - align-items: center; - padding: 5px 12px; - border-radius: $r-sm; - font-family: $font-sans; - font-size: 12.5px; - font-weight: 600; - letter-spacing: .2px; - white-space: nowrap; -} - -// Компактный вариант — для ячейки таблицы (RequestsTableView/SearchView), -// повторяет размеры, что были у их собственной .status до объединения. -.sm { - padding: 3px 10px; - border-radius: $r-md; - font-size: $fs-caption; - font-weight: 600; - letter-spacing: normal; -} - -.draft { - background: $gray-100; - color: $gray-700; -} - -.queued { - background: $gray-100; - color: $gray-500; -} - -.onApproval { - background: $blue-50; - color: $blue-600; -} - -.approved { - background: $green-50; - color: $green-600; -} - -.rejected { - background: $red-50; - color: $red-500; -} - -.returned { - background: $gold-50; - color: $amber-700; -} - -.execution { - background: $navy-50; - color: $navy-800; -} - -.closed { - background: $gray-100; - color: $gray-500; -} diff --git a/src/webparts/requestPortal/shared/approval/ui/StatusBadge.tsx b/src/webparts/requestPortal/shared/approval/ui/StatusBadge.tsx index 9f4d0cc..e611cda 100644 --- a/src/webparts/requestPortal/shared/approval/ui/StatusBadge.tsx +++ b/src/webparts/requestPortal/shared/approval/ui/StatusBadge.tsx @@ -1,40 +1,33 @@ import * as React from 'react'; -import styles from './StatusBadge.module.scss'; +import { Badge, BadgeVariant } from '@tnsu/ui-kit'; import { REQUEST_STATUS } from '../models/statuses'; -// Цвет бейджа по статусу заявки. Неизвестный статус → нейтральный (как черновик). -const VARIANT: Record = { - [REQUEST_STATUS.draft]: styles.draft, - [REQUEST_STATUS.queued]: styles.queued, - [REQUEST_STATUS.onApproval]: styles.onApproval, - [REQUEST_STATUS.onExecution]: styles.onApproval, // активный шаг — как «на согласовании» - [REQUEST_STATUS.approved]: styles.approved, - [REQUEST_STATUS.executed]: styles.approved, // выполнено — как «согласовано» (зелёный) - [REQUEST_STATUS.rejected]: styles.rejected, - [REQUEST_STATUS.returned]: styles.returned, - [REQUEST_STATUS.execution]: styles.execution, - [REQUEST_STATUS.closed]: styles.closed, +// Единственное место, где СТАТУС ЗАЯВКИ превращается в цвет. Сам цвет живёт в +// generic Badge (@tnsu/ui-kit) — здесь только смысловой маппинг «статус → variant». +// Раньше у таблиц была своя копия этой раскраски (statusClass) — она разошлась с +// этой, поэтому обе копии убраны в пользу общего компонента. +const STATUS_VARIANT: Record = { + [REQUEST_STATUS.draft]: 'neutral', + [REQUEST_STATUS.queued]: 'muted', + [REQUEST_STATUS.onApproval]: 'info', + [REQUEST_STATUS.onExecution]: 'info', // активный шаг — как «на согласовании» + [REQUEST_STATUS.approved]: 'success', + [REQUEST_STATUS.executed]: 'success', // выполнено — как «согласовано» (зелёный) + [REQUEST_STATUS.rejected]: 'danger', + [REQUEST_STATUS.returned]: 'warning', + [REQUEST_STATUS.execution]: 'primary', + [REQUEST_STATUS.closed]: 'muted' }; export interface IStatusBadgeProps { status: string; - /** - * 'md' (по умолчанию) — шапка формы/просмотра заявки; 'sm' — компактный - * вариант для ячейки таблицы (RequestsTableView/SearchView). Цвет статуса - * не зависит от размера — единственный источник цвета: VARIANT выше. - */ + /** 'md' (по умолчанию) — шапка формы; 'sm' — компактный для ячейки таблицы. */ size?: 'sm' | 'md'; } -/** - * Цветная плашка статуса заявки — ЕДИНСТВЕННОЕ место, где статус превращается - * в цвет. У таблиц раньше была своя копия этой раскраски (statusClass в - * RequestsTableView.tsx/SearchView.tsx) — она разошлась с этой (на согласовании - * и возвращена на доработку оказались перепутаны местами), поэтому обе копии - * убраны в пользу общего компонента. - */ +/** Цветная плашка статуса заявки поверх generic Badge из @tnsu/ui-kit. */ export const StatusBadge: React.FC = ({ status, size = 'md' }) => ( - + {status} - + );