From c98a6656113356be72f15e4fb8782b75a9e0474e 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:09:11 +0500 Subject: [PATCH] =?UTF-8?q?refactor:=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=84=D0=BE=D1=80=D0=BC=20=D0=BD=D0=B0=20?= =?UTF-8?q?ui-kit,=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D0=B4?= =?UTF-8?q?=D1=83=D0=B1=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- create-spfx-uikit.ps1 | 315 ++++++++++++++++++ package-lock.json | 8 +- package.json | 2 +- .../components/CategoryTabs/CategoryTabs.tsx | 2 +- .../components/Drawer/Drawer.tsx | 2 +- .../components/RequestGrid/RequestGrid.tsx | 2 +- .../components/RequestPortal.tsx | 2 +- .../components/SideNav/SideNav.tsx | 3 +- .../features/submit/SubmitFeature.tsx | 2 +- src/webparts/requestPortal/models/types.ts | 2 +- .../shared/dictionary/ui/DepartmentPicker.tsx | 2 +- .../shared/dictionary/ui/ProjectPicker.tsx | 2 +- .../shared/ui/Alert/Alert.module.scss | 30 -- .../requestPortal/shared/ui/Alert/Alert.tsx | 34 -- .../AttachmentList/AttachmentList.module.scss | 59 ---- .../ui/AttachmentList/AttachmentList.tsx | 41 --- .../shared/ui/Button/Button.module.scss | 73 ---- .../requestPortal/shared/ui/Button/Button.tsx | 46 --- .../CollapseSection.module.scss | 62 ---- .../ui/CollapseSection/CollapseSection.tsx | 32 -- .../ConfirmDialog/ConfirmDialog.module.scss | 98 ------ .../shared/ui/ConfirmDialog/ConfirmDialog.tsx | 82 ----- .../shared/ui/ConfirmDialog/confirmModal.ts | 44 --- .../ui/FileDropzone/FileDropzone.module.scss | 75 ----- .../shared/ui/FileDropzone/FileDropzone.tsx | 87 ----- .../ui/FormActions/FormActions.module.scss | 15 - .../shared/ui/FormActions/FormActions.tsx | 11 - .../shared/ui/FormCard/FormCard.module.scss | 42 --- .../shared/ui/FormCard/FormCard.tsx | 30 -- .../shared/ui/FormField/FormField.module.scss | 23 -- .../shared/ui/FormField/FormField.tsx | 26 -- .../shared/ui/FormGrid/FormGrid.module.scss | 11 - .../shared/ui/FormGrid/FormGrid.tsx | 11 - .../requestPortal/shared/ui/FormMode.tsx | 16 - .../shared/ui/Icon/Icon.module.scss | 22 -- .../requestPortal/shared/ui/Icon/Icon.tsx | 30 -- .../shared/ui/Icon/IconSprite.tsx | 140 -------- .../requestPortal/shared/ui/Icon/iconNames.ts | 37 -- .../shared/ui/ItemsTable/ItemsTable.tsx | 5 +- .../shared/ui/MoneyInput/MoneyInput.tsx | 31 -- .../shared/ui/PeopleField/PeopleField.tsx | 2 +- .../ui/PeopleField/PeopleFieldMulti.tsx | 2 +- .../shared/ui/PhoneInput/PhoneInput.tsx | 45 --- .../shared/ui/PrintButton/PrintButton.tsx | 117 ------- .../RequestNumber/RequestNumber.module.scss | 16 - .../shared/ui/RequestNumber/RequestNumber.tsx | 7 - .../shared/ui/Select/Select.module.scss | 20 -- .../requestPortal/shared/ui/Select/Select.tsx | 40 --- .../shared/ui/SubmitActions/SubmitActions.tsx | 4 +- .../shared/ui/TextArea/TextArea.module.scss | 21 -- .../shared/ui/TextArea/TextArea.tsx | 30 -- .../shared/ui/TextInput/TextInput.module.scss | 19 -- .../shared/ui/TextInput/TextInput.tsx | 39 --- .../shared/ui/Toggle/Toggle.module.scss | 65 ---- .../requestPortal/shared/ui/Toggle/Toggle.tsx | 35 -- src/webparts/requestPortal/shared/ui/index.ts | 55 +-- .../requestPortal/shared/unsavedChanges.ts | 2 +- 57 files changed, 367 insertions(+), 1709 deletions(-) create mode 100644 create-spfx-uikit.ps1 delete mode 100644 src/webparts/requestPortal/shared/ui/Alert/Alert.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/Alert/Alert.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/AttachmentList/AttachmentList.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/AttachmentList/AttachmentList.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/Button/Button.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/Button/Button.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/CollapseSection/CollapseSection.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/CollapseSection/CollapseSection.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/ConfirmDialog/ConfirmDialog.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/ConfirmDialog/ConfirmDialog.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/ConfirmDialog/confirmModal.ts delete mode 100644 src/webparts/requestPortal/shared/ui/FileDropzone/FileDropzone.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/FileDropzone/FileDropzone.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/FormActions/FormActions.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/FormActions/FormActions.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/FormCard/FormCard.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/FormCard/FormCard.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/FormField/FormField.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/FormField/FormField.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/FormGrid/FormGrid.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/FormGrid/FormGrid.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/FormMode.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/Icon/Icon.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/Icon/Icon.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/Icon/IconSprite.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/Icon/iconNames.ts delete mode 100644 src/webparts/requestPortal/shared/ui/MoneyInput/MoneyInput.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/PhoneInput/PhoneInput.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/PrintButton/PrintButton.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/Select/Select.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/Select/Select.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/TextArea/TextArea.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/TextArea/TextArea.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/TextInput/TextInput.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/TextInput/TextInput.tsx delete mode 100644 src/webparts/requestPortal/shared/ui/Toggle/Toggle.module.scss delete mode 100644 src/webparts/requestPortal/shared/ui/Toggle/Toggle.tsx diff --git a/create-spfx-uikit.ps1 b/create-spfx-uikit.ps1 new file mode 100644 index 0000000..4b1b1cb --- /dev/null +++ b/create-spfx-uikit.ps1 @@ -0,0 +1,315 @@ +# ============================================================= +# SPFx + Heft project bootstrap script (PowerShell) +# ВАРИАНТ С ПОДКЛЮЧЕНИЕМ ОБЩЕЙ UI-БИБЛИОТЕКИ @tnsu/ui-kit. +# Запустите из корня вашей ПУСТОЙ папки проекта: +# .\create-spfx-uikit.ps1 +# +# Если запуск скриптов заблокирован, выполните сначала: +# Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned +# +# Требование: у запускающего должен быть токен @tnsu в ~/.npmrc +# (см. README пакета tnsu-ui-kit) — иначе установка @tnsu/ui-kit не пройдёт. +# ============================================================= + +$ErrorActionPreference = "Stop" + +# ---- Цвета вывода ---- +function Info { param($msg) Write-Host "[INFO] $msg" -ForegroundColor Cyan } +function Success { param($msg) Write-Host "[OK] $msg" -ForegroundColor Green } +function Warn { param($msg) Write-Host "[WARN] $msg" -ForegroundColor Yellow } +function Header { param($msg) Write-Host "`n$msg" -ForegroundColor White } + +Header "=================================================" +Header " SPFx + Heft — автоматическая инициализация" +Header " (с общей UI-библиотекой @tnsu/ui-kit)" +Header "=================================================`n" + +# --------------------------------------------------------- +# 1. Проверка Node.js (SPFx 1.20+ требует Node 18+) +# --------------------------------------------------------- +Info "Проверка Node.js..." + +try { + $nodeVersion = node -e "process.stdout.write(process.version)" 2>&1 +} catch { + Warn "Node.js не найден. Установите Node 18 LTS: https://nodejs.org" + exit 1 +} + +$nodeMajor = [int]($nodeVersion.TrimStart('v').Split('.')[0]) +if ($nodeMajor -lt 18) { + Warn "Обнаружен Node.js $nodeVersion. SPFx 1.20+ требует Node 18.x или выше." + Warn "Установите Node 18 LTS через nvm-windows или с https://nodejs.org" + exit 1 +} +Success "Node.js $nodeVersion — подходит." + +# --------------------------------------------------------- +# 2. Вспомогательная функция: установка глобального пакета +# --------------------------------------------------------- +function Install-GlobalIfMissing { + param( + [string]$Package, + [string]$Command = "" + ) + if ($Command -eq "") { $Command = $Package.Split('/')[-1] } + + if (-not (Get-Command $Command -ErrorAction SilentlyContinue)) { + Info "Устанавливаем $Package глобально..." + npm install -g $Package + Success "$Package установлен." + } else { + Success "$Package уже установлен." + } +} + +Install-GlobalIfMissing "@microsoft/generator-sharepoint" "yo" +Install-GlobalIfMissing "yo" "yo" +Install-GlobalIfMissing "@rushstack/heft" "heft" + +# --------------------------------------------------------- +# 3. Создание package.json +# --------------------------------------------------------- +Info "Создаём package.json..." + +$packageJson = @' +{ + "name": "spfx-heft-project", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "heft build", + "bundle": "heft build && gulp bundle --ship", + "package-solution": "heft build && gulp package-solution --ship", + "serve": "heft build && gulp serve", + "clean": "heft clean" + }, + "overrides": { + "ajv": "^8.0.0" + } +} +'@ + +$packageJson | Set-Content -Encoding UTF8 "package.json" +Success "package.json создан." + +# --------------------------------------------------------- +# 4. Создание структуры папок +# --------------------------------------------------------- +Info "Создаём структуру папок..." +New-Item -ItemType Directory -Force -Path "config" | Out-Null +New-Item -ItemType Directory -Force -Path "src\webparts" | Out-Null +Success "Папки созданы." + +# --------------------------------------------------------- +# 5. Запуск Yeoman-генератора SPFx +# --------------------------------------------------------- +Info "Запускаем Yeoman-генератор SPFx..." +Write-Host "" +Write-Host ">>> Сейчас откроется интерактивный генератор Yeoman." -ForegroundColor Yellow +Write-Host ">>> Выберите тип: WebPart, Framework: No framework (или React)." -ForegroundColor Yellow +Write-Host "" + +yo "@microsoft/sharepoint" + +npm uninstall @microsoft/sp-build-web ajv gulp +npm uninstall @microsoft/rush-stack-compiler-5.3 + +# --------------------------------------------------------- +# 6. Установка Heft и Rush Stack зависимостей +# --------------------------------------------------------- +Info "Устанавливаем Heft и плагины..." + +$devDeps = @( + "@microsoft/spfx-web-build-rig@1.22.1", + "@microsoft/spfx-heft-plugins@1.22.1", + "@microsoft/eslint-config-spfx@1.22.1", + "@microsoft/eslint-plugin-spfx@1.22.17", + "@microsoft/sp-module-interfaces@1.22.1", + "@rushstack/eslint-config@4.5.2", + "@rushstack/heft@1.1.2", + "@types/heft-jest@1.0.2", + "@typescript-eslint/parser@8.46.2" +) + +npm install --save-dev @($devDeps) --save-exact --force --legacy-peer-deps +npm install typescript@~5.8.0 --save-dev + +Success "Heft и плагины установлены." + +# --------------------------------------------------------- +# 7. config/rig.json +# --------------------------------------------------------- +Info "Создаём config\rig.json..." +New-Item -ItemType Directory -Force -Path "config" | Out-Null + +$rigJson = @' +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@microsoft/spfx-web-build-rig" +} +'@ + +$rigJson | Set-Content -Encoding UTF8 "config\rig.json" +Success "config\rig.json создан." + +# --------------------------------------------------------- +# 8. config/sass.json +# --------------------------------------------------------- +Info "Создаём config\sass.json..." +New-Item -ItemType Directory -Force -Path "config" | Out-Null + +$sassJson = @' +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-sass-plugin.schema.json", + "extends": "@microsoft/spfx-web-build-rig/profiles/default/config/sass.json" +} +'@ + +$sassJson | Set-Content -Encoding UTF8 "config\sass.json" +Success "config\sass.json создан." + +# --------------------------------------------------------- +# 9. config/typescript.json +# --------------------------------------------------------- +Info "Создаём config\typescript.json..." +New-Item -ItemType Directory -Force -Path "config" | Out-Null + +$typescriptJson = @' +{ + "extends": "@microsoft/spfx-web-build-rig/profiles/default/config/typescript.json", + "staticAssetsToCopy": { + "fileExtensions": [".resx", ".jpg", ".png", ".woff", ".eot", ".ttf", ".svg", ".gif"], + "includeGlobs": ["webparts/*/loc/*.js"] + } +} +'@ + +$typescriptJson | Set-Content -Encoding UTF8 "config\typescript.json" +Success "config\typescript.json создан." + +# --------------------------------------------------------- +# 8. tsconfig.json (если Yeoman ещё не создал) +# --------------------------------------------------------- +if (-not (Test-Path "tsconfig.json")) { + Info "Создаём tsconfig.json..." + + $tsConfig = @' +{ + "extends": "./node_modules/@microsoft/spfx-web-build-rig/profiles/default/tsconfig-base.json" +} +'@ + + $tsConfig | Set-Content -Encoding UTF8 "tsconfig.json" + Success "tsconfig.json создан." +} else { + Success "tsconfig.json уже существует, пропускаем." +} + +# --------------------------------------------------------- +# 9. .eslintrc.js (если не создан) +# --------------------------------------------------------- +if (-not (Test-Path ".eslintrc.js")) { + Info "Создаём .eslintrc.js..." + + $eslintConfig = @' +require('@rushstack/eslint-config/patch/modern-module-resolution'); + +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/default'], + parserOptions: { tsconfigRootDir: __dirname } +}; +'@ + + $eslintConfig | Set-Content -Encoding UTF8 ".eslintrc.js" + Success ".eslintrc.js создан." +} else { + Success ".eslintrc.js уже существует, пропускаем." +} + +# --------------------------------------------------------- +# 10. .gitignore +# --------------------------------------------------------- +Info "Создаём .gitignore..." + +$gitignore = @' +node_modules/ +lib/ +dist/ +temp/ +*.sppkg +.heft/ +sharepoint/solution/ +'@ + +$gitignore | Set-Content -Encoding UTF8 ".gitignore" +Success ".gitignore создан." + +# --------------------------------------------------------- +# 11. Финальный npm install +# --------------------------------------------------------- +$devDeps = @( + "@microsoft/sp-component-base@1.22.1", + "@microsoft/sp-core-library@1.22.1", + "@microsoft/sp-lodash-subset@1.22.1", + "@microsoft/sp-office-ui-fabric-core@1.22.1", + "@microsoft/sp-property-pane@1.22.1", + "@microsoft/sp-webpart-base@1.22.1" +) + +Info "Финальный npm install..." +npm install @($devDeps) --save-exact --legacy-peer-deps +Success "Все зависимости установлены." + +# --------------------------------------------------------- +# 12. Подключение общей UI-библиотеки @tnsu/ui-kit +# --------------------------------------------------------- +Info "Подключаем @tnsu/ui-kit..." + +# .npmrc: адрес registry для скоупа @tnsu (НЕ секрет, можно коммитить). +# Токен на чтение — у каждого разработчика свой, в ~/.npmrc (в проект не кладём). +$npmrc = @' +@tnsu:registry=http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/ +'@ +$npmrc | Set-Content -Encoding UTF8 ".npmrc" +Success ".npmrc создан (адрес registry @tnsu)." + +try { + npm install @tnsu/ui-kit + Success "@tnsu/ui-kit установлен." + Write-Host " Импорт: import { Button } from '@tnsu/ui-kit';" -ForegroundColor DarkGray + Write-Host " Стили: import '@tnsu/ui-kit/styles.css'; (один раз в корневом компоненте)" -ForegroundColor DarkGray +} catch { + Warn "Не удалось поставить @tnsu/ui-kit. Скорее всего нет токена @tnsu в ~/.npmrc." + Warn "Создай токен (Gitea → Настройки → Приложения → package: Read) и пропиши в ~/.npmrc:" + Warn " //git.tnsu.kz/api/packages/timur.gorodetskiy/npm/:_authToken=ТВОЙ_ТОКЕН" + Warn "Потом выполни: npm install @tnsu/ui-kit" +} + +# --------------------------------------------------------- +# Готово +# --------------------------------------------------------- +Write-Host "" +Write-Host "=================================================" -ForegroundColor Green +Write-Host " Проект успешно инициализирован!" -ForegroundColor Green +Write-Host "=================================================" -ForegroundColor Green +Write-Host "" +Write-Host " Доступные команды:" -ForegroundColor White +Write-Host "" +Write-Host " npm run serve " -NoNewline -ForegroundColor Cyan +Write-Host "— локальная разработка (gulp serve)" +Write-Host " npm run build " -NoNewline -ForegroundColor Cyan +Write-Host "— сборка через Heft" +Write-Host " npm run bundle " -NoNewline -ForegroundColor Cyan +Write-Host "— бандлинг для продакшна" +Write-Host " npm run package-solution " -NoNewline -ForegroundColor Cyan +Write-Host "— создание .sppkg пакета" +Write-Host " npm run clean " -NoNewline -ForegroundColor Cyan +Write-Host "— очистка артефактов сборки" +Write-Host "" +Write-Host " UI-библиотека: " -NoNewline -ForegroundColor Yellow +Write-Host "@tnsu/ui-kit подключён (см. .npmrc)" -ForegroundColor Cyan +Write-Host "" +Write-Host " Следующий шаг: " -NoNewline -ForegroundColor Yellow +Write-Host "npm run serve" -ForegroundColor Cyan +Write-Host "" diff --git a/package-lock.json b/package-lock.json index 42d5f0a..0242ff2 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.0.1", + "@tnsu/ui-kit": "^0.2.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.0.1", - "resolved": "http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/%40tnsu%2Fui-kit/-/0.0.1/ui-kit-0.0.1.tgz", - "integrity": "sha512-fdw5KEsahQ10qT/vf/ZWvwMHorcfk7ketUkvgUa8Zlg8imVWTCq1qxxEkS439qXPZw/vTtYYpkSzgM1JOGOFNg==", + "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==", "license": "ISC", "peerDependencies": { "@fluentui/react": "^8.106.4", diff --git a/package.json b/package.json index 112b0d3..f9bc860 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.0.1", + "@tnsu/ui-kit": "^0.2.0", "react": "17.0.1", "react-dom": "17.0.1", "tslib": "2.3.1" diff --git a/src/webparts/requestPortal/components/CategoryTabs/CategoryTabs.tsx b/src/webparts/requestPortal/components/CategoryTabs/CategoryTabs.tsx index 2a4143b..5538167 100644 --- a/src/webparts/requestPortal/components/CategoryTabs/CategoryTabs.tsx +++ b/src/webparts/requestPortal/components/CategoryTabs/CategoryTabs.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import styles from './CategoryTabs.module.scss'; -import { Icon } from '../../shared/ui/Icon/Icon'; +import { Icon } from '@tnsu/ui-kit'; import { ICategory } from '../../models/types'; interface ICategoryTabsProps { diff --git a/src/webparts/requestPortal/components/Drawer/Drawer.tsx b/src/webparts/requestPortal/components/Drawer/Drawer.tsx index 7406628..2f74364 100644 --- a/src/webparts/requestPortal/components/Drawer/Drawer.tsx +++ b/src/webparts/requestPortal/components/Drawer/Drawer.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import styles from './Drawer.module.scss'; -import { Icon } from '../../shared/ui/Icon/Icon'; +import { Icon } from '@tnsu/ui-kit'; interface IDrawerProps { title: string; diff --git a/src/webparts/requestPortal/components/RequestGrid/RequestGrid.tsx b/src/webparts/requestPortal/components/RequestGrid/RequestGrid.tsx index 2dfbcfc..612635d 100644 --- a/src/webparts/requestPortal/components/RequestGrid/RequestGrid.tsx +++ b/src/webparts/requestPortal/components/RequestGrid/RequestGrid.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import styles from './RequestGrid.module.scss'; -import { Icon } from '../../shared/ui/Icon/Icon'; +import { Icon } from '@tnsu/ui-kit'; import { IRequestType } from '../../models/types'; interface IRequestGridProps { diff --git a/src/webparts/requestPortal/components/RequestPortal.tsx b/src/webparts/requestPortal/components/RequestPortal.tsx index 8b99a93..f99288d 100644 --- a/src/webparts/requestPortal/components/RequestPortal.tsx +++ b/src/webparts/requestPortal/components/RequestPortal.tsx @@ -2,7 +2,7 @@ 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 '../shared/ui/Icon/IconSprite'; +import { IconSprite } from '@tnsu/ui-kit'; import { ConfirmHost } from '../shared/ui'; import { navigate, diff --git a/src/webparts/requestPortal/components/SideNav/SideNav.tsx b/src/webparts/requestPortal/components/SideNav/SideNav.tsx index 8f207bb..581b343 100644 --- a/src/webparts/requestPortal/components/SideNav/SideNav.tsx +++ b/src/webparts/requestPortal/components/SideNav/SideNav.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import styles from './SideNav.module.scss'; -import { Icon } from '../../shared/ui/Icon/Icon'; -import { IconName } from '../../shared/ui/Icon/iconNames'; +import { Icon, IconName } from '@tnsu/ui-kit'; import { NavSection } from '../../shared/routing'; interface INavItem { diff --git a/src/webparts/requestPortal/features/submit/SubmitFeature.tsx b/src/webparts/requestPortal/features/submit/SubmitFeature.tsx index 9dd3109..a50b9af 100644 --- a/src/webparts/requestPortal/features/submit/SubmitFeature.tsx +++ b/src/webparts/requestPortal/features/submit/SubmitFeature.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { useState, useEffect, useMemo, useRef } from 'react'; import type { WebPartContext } from '@microsoft/sp-webpart-base'; import styles from './SubmitFeature.module.scss'; -import { Icon } from '../../shared/ui/Icon/Icon'; +import { Icon } from '@tnsu/ui-kit'; import { RequestGrid } from '../../components/RequestGrid/RequestGrid'; import { getRequestsByCategory, REQUEST_TYPES } from '../../data/requests'; import { getFormComponent } from '../../forms/registry'; diff --git a/src/webparts/requestPortal/models/types.ts b/src/webparts/requestPortal/models/types.ts index a543ec2..3b053be 100644 --- a/src/webparts/requestPortal/models/types.ts +++ b/src/webparts/requestPortal/models/types.ts @@ -1,5 +1,5 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; -import { IconName } from '../shared/ui/Icon/iconNames'; +import { IconName } from '@tnsu/ui-kit'; /** Идентификатор типа заявки — ключ в реестре форм (formRegistry). */ export type RequestTypeId = string; diff --git a/src/webparts/requestPortal/shared/dictionary/ui/DepartmentPicker.tsx b/src/webparts/requestPortal/shared/dictionary/ui/DepartmentPicker.tsx index 4bd68af..ed8c2af 100644 --- a/src/webparts/requestPortal/shared/dictionary/ui/DepartmentPicker.tsx +++ b/src/webparts/requestPortal/shared/dictionary/ui/DepartmentPicker.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { WebPartContext } from '@microsoft/sp-webpart-base'; import styles from './DepartmentPicker.module.scss'; -import { useFormReadOnly } from '../../ui/FormMode'; +import { useFormReadOnly } from '@tnsu/ui-kit'; import { getDepartments } from '../departments.service'; import { IDepartment } from '../models/IDepartment'; diff --git a/src/webparts/requestPortal/shared/dictionary/ui/ProjectPicker.tsx b/src/webparts/requestPortal/shared/dictionary/ui/ProjectPicker.tsx index 46e5be0..59e4c8b 100644 --- a/src/webparts/requestPortal/shared/dictionary/ui/ProjectPicker.tsx +++ b/src/webparts/requestPortal/shared/dictionary/ui/ProjectPicker.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { WebPartContext } from '@microsoft/sp-webpart-base'; import styles from './ProjectPicker.module.scss'; -import { useFormReadOnly } from '../../ui/FormMode'; +import { useFormReadOnly } from '@tnsu/ui-kit'; import { getProjects } from '../projects.service'; import { IProject } from '../models/IProject'; diff --git a/src/webparts/requestPortal/shared/ui/Alert/Alert.module.scss b/src/webparts/requestPortal/shared/ui/Alert/Alert.module.scss deleted file mode 100644 index 4019255..0000000 --- a/src/webparts/requestPortal/shared/ui/Alert/Alert.module.scss +++ /dev/null @@ -1,30 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.alert { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 16px; - border: 1px solid; - border-radius: $r-md; - font-size: 12.5px; - margin-bottom: 18px; -} - -.warn { - background: $gold-50; - color: $amber-700; - border-color: $gold-300; -} - -.ok { - background: $green-50; - color: $green-600; - border-color: #bbf7d0; -} - -.err { - background: $red-50; - color: $red-500; - border-color: #fecaca; -} diff --git a/src/webparts/requestPortal/shared/ui/Alert/Alert.tsx b/src/webparts/requestPortal/shared/ui/Alert/Alert.tsx deleted file mode 100644 index 9eaccc1..0000000 --- a/src/webparts/requestPortal/shared/ui/Alert/Alert.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import styles from './Alert.module.scss'; -import { Icon } from '../Icon/Icon'; -import { IconName } from '../Icon/iconNames'; - -type AlertVariant = 'warn' | 'ok' | 'err'; - -interface IAlertProps { - variant: AlertVariant; - children: React.ReactNode; -} - -const variantClass: Record = { - warn: styles.warn, - ok: styles.ok, - err: styles.err -}; - -const variantIcon: Record = { - warn: 'warning', - ok: 'check', - err: 'close' -}; - -export const Alert: React.FC = ({ variant, children }) => { - const classes = [styles.alert, variantClass[variant]].join(' '); - - return ( -
- - {children} -
- ); -}; diff --git a/src/webparts/requestPortal/shared/ui/AttachmentList/AttachmentList.module.scss b/src/webparts/requestPortal/shared/ui/AttachmentList/AttachmentList.module.scss deleted file mode 100644 index 79da16f..0000000 --- a/src/webparts/requestPortal/shared/ui/AttachmentList/AttachmentList.module.scss +++ /dev/null @@ -1,59 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.list { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 6px; -} - -.item { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 12px; - border: 1px solid $gray-200; - border-radius: $r-sm; - background: $gray-50; - font-size: 13px; -} - -.link { - flex: 1; - min-width: 0; - color: $navy-700; - text-decoration: none; - - &:hover { - color: $gold-600; - text-decoration: underline; - } -} - -.removeBtn { - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - padding: 0; - border: 1px solid $gray-300; - border-radius: $r-sm; - background: $white; - color: $gray-500; - cursor: pointer; - transition: .16s; - - &:hover { - border-color: $red-500; - color: $red-500; - } -} - -.empty { - color: $gray-500; - font-size: $fs-sm; -} diff --git a/src/webparts/requestPortal/shared/ui/AttachmentList/AttachmentList.tsx b/src/webparts/requestPortal/shared/ui/AttachmentList/AttachmentList.tsx deleted file mode 100644 index b303e3a..0000000 --- a/src/webparts/requestPortal/shared/ui/AttachmentList/AttachmentList.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from 'react'; -import styles from './AttachmentList.module.scss'; -import { Icon } from '../Icon/Icon'; -import { IAttachmentInfo } from '../../attachments/attachments.service'; - -interface IAttachmentListProps { - items: IAttachmentInfo[]; - /** Если задан — рядом с файлом показывается кнопка удаления (режим правки). */ - onRemove?: (fileName: string) => void; -} - -/** Список вложений заявки как ссылки на скачивание; с onRemove — ещё и удаление. */ -export const AttachmentList: React.FC = ({ items, onRemove }) => { - if (items.length === 0) { - return

Вложений нет.

; - } - - return ( -
    - {items.map((attachment) => ( -
  • - - - {attachment.fileName} - - {onRemove && ( - - )} -
  • - ))} -
- ); -}; diff --git a/src/webparts/requestPortal/shared/ui/Button/Button.module.scss b/src/webparts/requestPortal/shared/ui/Button/Button.module.scss deleted file mode 100644 index ab86075..0000000 --- a/src/webparts/requestPortal/shared/ui/Button/Button.module.scss +++ /dev/null @@ -1,73 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 7px; - padding: 10px 20px; - border: 1px solid transparent; - border-radius: $r-sm; - font-family: $font-sans; - font-size: 13px; - font-weight: 500; - letter-spacing: .15px; - white-space: nowrap; - cursor: pointer; - transition: .18s; - - &:disabled { - opacity: .55; - cursor: default; - pointer-events: none; - } -} - -.sm { - padding: 7px 14px; - font-size: 12.5px; -} - -.full { - width: 100%; -} - -.primary { - background: $navy-800; - border-color: $navy-800; - color: #fff; - - &:hover { background: $navy-700; box-shadow: $sh-2; } -} - -.accent { - background: $grad-gold; - border-color: $gold-500; - color: $navy-950; - font-weight: 600; - - &:hover { box-shadow: $sh-gold; transform: translateY(-1px); } -} - -.secondary { - background: $white; - border-color: $gray-300; - color: $navy-800; - - &:hover { border-color: $gold-500; color: $navy-900; } -} - -.ghost { - background: transparent; - color: $navy-700; - - &:hover { background: $navy-100; } -} - -.danger { - background: $white; - border-color: $gray-300; - color: $red-500; - - &:hover { border-color: $red-500; background: $red-50; } -} diff --git a/src/webparts/requestPortal/shared/ui/Button/Button.tsx b/src/webparts/requestPortal/shared/ui/Button/Button.tsx deleted file mode 100644 index 47bef7b..0000000 --- a/src/webparts/requestPortal/shared/ui/Button/Button.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import styles from './Button.module.scss'; - -type ButtonVariant = 'primary' | 'accent' | 'secondary' | 'ghost' | 'danger'; -type ButtonSize = 'md' | 'sm'; - -interface IButtonProps extends React.ButtonHTMLAttributes { - variant?: ButtonVariant; - size?: ButtonSize; - fullWidth?: boolean; -} - -const variantClass: Record = { - primary: styles.primary, - accent: styles.accent, - secondary: styles.secondary, - ghost: styles.ghost, - danger: styles.danger -}; - -export const Button: React.FC = ({ - variant = 'secondary', - size = 'md', - fullWidth = false, - type = 'button', - className, - children, - ...rest -}) => { - const classes = [ - styles.btn, - variantClass[variant], - size === 'sm' ? styles.sm : '', - fullWidth ? styles.full : '', - className - ] - .filter(Boolean) - .join(' '); - - // type по умолчанию 'button', чтобы кнопки внутри формы не сабмитили её случайно. - return ( - - ); -}; diff --git a/src/webparts/requestPortal/shared/ui/CollapseSection/CollapseSection.module.scss b/src/webparts/requestPortal/shared/ui/CollapseSection/CollapseSection.module.scss deleted file mode 100644 index 0938f89..0000000 --- a/src/webparts/requestPortal/shared/ui/CollapseSection/CollapseSection.module.scss +++ /dev/null @@ -1,62 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.section { - margin-top: 8px; -} - -.header { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 14px 0 10px; - border: 0; - border-top: 1px solid $gray-200; - background: transparent; - cursor: pointer; - - &::before { - content: ''; - position: absolute; - top: -1px; - left: 0; - width: 48px; - height: 1px; - background: $gold-500; - } -} - -.title { - display: flex; - align-items: center; - gap: 10px; - font-family: $font-serif; - font-size: 14.5px; - font-weight: 600; - color: $navy-900; - letter-spacing: .2px; - - svg { color: $gold-600; } -} - -.chevron { - display: flex; - color: $gray-400; - transition: transform .2s; -} - -.open { - transform: rotate(180deg); -} - -.body { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px 22px; - padding-top: 16px; - - @media (max-width: 640px) { - grid-template-columns: 1fr; - } -} diff --git a/src/webparts/requestPortal/shared/ui/CollapseSection/CollapseSection.tsx b/src/webparts/requestPortal/shared/ui/CollapseSection/CollapseSection.tsx deleted file mode 100644 index 898a3f3..0000000 --- a/src/webparts/requestPortal/shared/ui/CollapseSection/CollapseSection.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react'; -import { useState } from 'react'; -import styles from './CollapseSection.module.scss'; -import { Icon } from '../Icon/Icon'; -import { IconName } from '../Icon/iconNames'; - -interface ICollapseSectionProps { - title: string; - icon: IconName; - defaultOpen?: boolean; - children: React.ReactNode; -} - -/** Сворачиваемая секция формы: заголовок с иконкой и шевроном + тело. */ -export const CollapseSection: React.FC = ({ title, icon, defaultOpen = true, children }) => { - const [open, setOpen] = useState(defaultOpen); - - return ( -
- - {open &&
{children}
} -
- ); -}; diff --git a/src/webparts/requestPortal/shared/ui/ConfirmDialog/ConfirmDialog.module.scss b/src/webparts/requestPortal/shared/ui/ConfirmDialog/ConfirmDialog.module.scss deleted file mode 100644 index 53b43cf..0000000 --- a/src/webparts/requestPortal/shared/ui/ConfirmDialog/ConfirmDialog.module.scss +++ /dev/null @@ -1,98 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -// Поверх всего, включая SPFx-хром вебпарта. -.overlay { - position: fixed; - inset: 0; - z-index: 999997; - display: flex; - align-items: center; - justify-content: center; - padding: $sp-6; - background: rgba($navy-950, .55); - font-family: $font-sans; - // Плавное появление подложки. - animation: overlayIn .14s ease-out; -} - -.card { - box-sizing: border-box; - width: 100%; - max-width: 440px; - padding: $sp-5 $sp-5 $sp-4; - border-radius: $r-lg; - background: $white; - box-shadow: $sh-modal; - animation: cardIn .16s ease-out; -} - -.title { - font-family: $font-serif; - font-size: $fs-form-title; - font-weight: 600; - color: $navy-950; -} - -.message { - margin-top: $sp-2; - font-size: $fs-sm; - line-height: 1.5; - color: $gray-700; -} - -.actions { - display: flex; - justify-content: flex-end; - gap: $sp-2; - margin-top: $sp-5; -} - -.cancelBtn { - padding: 9px 18px; - border: 1px solid $gray-300; - border-radius: $r-sm; - background: $white; - color: $navy-800; - font-family: $font-sans; - font-size: $fs-sm; - cursor: pointer; - transition: .15s; - - &:hover { - border-color: $gold-400; - } -} - -.confirmBtn { - @include btn-accent; - padding: 9px 18px; - border-radius: $r-sm; - font-size: $fs-sm; - cursor: pointer; -} - -.confirmDanger { - padding: 9px 18px; - border: 0; - border-radius: $r-sm; - background: $red-500; - color: $white; - font-family: $font-sans; - font-size: $fs-sm; - cursor: pointer; - transition: .15s; - - &:hover { - filter: brightness(.94); - } -} - -@keyframes overlayIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes cardIn { - from { opacity: 0; transform: translateY(8px) scale(.98); } - to { opacity: 1; transform: translateY(0) scale(1); } -} diff --git a/src/webparts/requestPortal/shared/ui/ConfirmDialog/ConfirmDialog.tsx b/src/webparts/requestPortal/shared/ui/ConfirmDialog/ConfirmDialog.tsx deleted file mode 100644 index 36ba3ee..0000000 --- a/src/webparts/requestPortal/shared/ui/ConfirmDialog/ConfirmDialog.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import * as React from 'react'; -import { createPortal } from 'react-dom'; -import styles from './ConfirmDialog.module.scss'; -import { IConfirmOptions, registerConfirmHost } from './confirmModal'; - -interface IConfirmDialogProps extends IConfirmOptions { - onConfirm: () => void; - onCancel: () => void; -} - -/** Само окно подтверждения. Рендерится в document.body поверх всего (portal). */ -const ConfirmDialog: React.FC = ({ - title, - message, - confirmText = 'Продолжить', - cancelText = 'Отмена', - danger, - onConfirm, - onCancel, -}) => { - // Esc — отмена. Слушаем на всё окно, пока модалка открыта. - React.useEffect(() => { - const onKey = (e: KeyboardEvent): void => { - if (e.key === 'Escape') onCancel(); - }; - window.addEventListener('keydown', onKey); - return () => window.removeEventListener('keydown', onKey); - }, [onCancel]); - - return createPortal( - // Клик по фону = отмена; клик внутри карточки не всплывает до фона. -
-
e.stopPropagation()} - > - {title &&
{title}
} -
{message}
-
- - -
-
-
, - document.body - ); -}; - -/** - * Хост модалки: монтируется ОДИН раз в портале. Слушает императивные вызовы - * confirmModal(...) и показывает окно; ответ пользователя резолвит их промис. - */ -export const ConfirmHost: React.FC = () => { - const [state, setState] = React.useState<{ - opts: IConfirmOptions; - resolve: (ok: boolean) => void; - } | undefined>(undefined); - - React.useEffect(() => registerConfirmHost((opts, resolve) => setState({ opts, resolve })), []); - - if (!state) return null; - - const close = (ok: boolean): void => { - state.resolve(ok); - setState(undefined); - }; - - return ( - close(true)} onCancel={() => close(false)} /> - ); -}; diff --git a/src/webparts/requestPortal/shared/ui/ConfirmDialog/confirmModal.ts b/src/webparts/requestPortal/shared/ui/ConfirmDialog/confirmModal.ts deleted file mode 100644 index a48354f..0000000 --- a/src/webparts/requestPortal/shared/ui/ConfirmDialog/confirmModal.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Императивный вызов модалки подтверждения из любого места (в т.ч. из обычных -// модулей вроде shared/unsavedChanges.ts, где нет React-контекста). -// -// Модалка живёт в React-дереве (ConfirmHost, монтируется один раз в -// RequestPortal). Здесь — только «мостик»: confirmModal(...) отдаёт промис, -// который резолвится, когда пользователь нажмёт «Продолжить»/«Отмена». - -export interface IConfirmOptions { - /** Заголовок окна. */ - title?: string; - /** Основной текст-вопрос. */ - message: string; - /** Подпись подтверждающей кнопки. По умолчанию — «Продолжить». */ - confirmText?: string; - /** Подпись кнопки отмены. По умолчанию — «Отмена». */ - cancelText?: string; - /** Опасное действие (потеря данных) — подтверждающая кнопка красная. */ - danger?: boolean; -} - -type ConfirmHandler = (opts: IConfirmOptions, resolve: (ok: boolean) => void) => void; - -// Единственный подписчик — смонтированный ConfirmHost. Больше одного окна -// подтверждения одновременно нам не нужно. -let handler: ConfirmHandler | undefined; - -export function registerConfirmHost(fn: ConfirmHandler): () => void { - handler = fn; - return () => { - if (handler === fn) handler = undefined; - }; -} - -/** - * Показать модальное подтверждение. Промис резолвится в true («Продолжить») или - * false («Отмена»/Esc/клик по фону). Если хост почему-то не смонтирован — - * падаем на нативный confirm, чтобы не потерять защиту от потери данных. - */ -export function confirmModal(opts: IConfirmOptions): Promise { - if (!handler) { - return Promise.resolve(window.confirm(opts.message)); - } - return new Promise((resolve) => handler!(opts, resolve)); -} diff --git a/src/webparts/requestPortal/shared/ui/FileDropzone/FileDropzone.module.scss b/src/webparts/requestPortal/shared/ui/FileDropzone/FileDropzone.module.scss deleted file mode 100644 index a4c8a1b..0000000 --- a/src/webparts/requestPortal/shared/ui/FileDropzone/FileDropzone.module.scss +++ /dev/null @@ -1,75 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.zone { - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - padding: 24px; - border: 1.5px dashed $gray-300; - border-radius: $r-md; - color: $gray-400; - font-size: 12.5px; - cursor: pointer; - transition: .18s; - - &:hover, - &.over { - border-color: $gold-500; - color: $gold-600; - background: $gold-50; - } -} - -.hidden { - display: none; -} - -.list { - list-style: none; - margin: 10px 0 0; - padding: 0; - display: flex; - flex-direction: column; - gap: 6px; -} - -.item { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 12px; - border: 1px solid $gray-200; - border-radius: $r-sm; - background: $gray-50; - font-size: 13px; - color: $gray-700; -} - -.name { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.size { - color: $gray-400; - font-size: 11.5px; - flex-shrink: 0; -} - -.remove { - display: inline-flex; - padding: 2px; - border: 0; - background: transparent; - color: $gray-400; - cursor: pointer; - flex-shrink: 0; - - &:hover { - color: $red-500; - } -} diff --git a/src/webparts/requestPortal/shared/ui/FileDropzone/FileDropzone.tsx b/src/webparts/requestPortal/shared/ui/FileDropzone/FileDropzone.tsx deleted file mode 100644 index 6d7be79..0000000 --- a/src/webparts/requestPortal/shared/ui/FileDropzone/FileDropzone.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import * as React from 'react'; -import { useState, useRef } from 'react'; -import styles from './FileDropzone.module.scss'; -import { Icon } from '../Icon/Icon'; - -interface IFileDropzoneProps { - files: File[]; - onChange: (files: File[]) => void; -} - -function formatSize(bytes: number): string { - if (bytes < 1024) return `${bytes} Б`; - if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} КБ`; - return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`; -} - -/** Зона выбора файлов (перетащить/кликнуть) со списком выбранных и удалением. */ -export const FileDropzone: React.FC = ({ files, onChange }) => { - const inputRef = useRef(null); - const [dragOver, setDragOver] = useState(false); - - const addFiles = (incoming: FileList | null): void => { - if (!incoming || incoming.length === 0) { - return; - } - onChange([...files, ...Array.from(incoming)]); - }; - - const removeAt = (index: number): void => { - onChange(files.filter((_, i) => i !== index)); - }; - - const zoneClasses = [styles.zone, dragOver ? styles.over : ''].filter(Boolean).join(' '); - - return ( -
-
inputRef.current && inputRef.current.click()} - onDragOver={(e) => { - e.preventDefault(); - setDragOver(true); - }} - onDragLeave={() => setDragOver(false)} - onDrop={(e) => { - e.preventDefault(); - setDragOver(false); - addFiles(e.dataTransfer.files); - }} - > - - Нажмите или перетащите файлы сюда -
- - { - addFiles(e.target.files); - e.target.value = ''; // чтобы можно было выбрать тот же файл повторно - }} - /> - - {files.length > 0 && ( -
    - {files.map((file, index) => ( -
  • - - {file.name} - {formatSize(file.size)} - -
  • - ))} -
- )} -
- ); -}; diff --git a/src/webparts/requestPortal/shared/ui/FormActions/FormActions.module.scss b/src/webparts/requestPortal/shared/ui/FormActions/FormActions.module.scss deleted file mode 100644 index 68b10f2..0000000 --- a/src/webparts/requestPortal/shared/ui/FormActions/FormActions.module.scss +++ /dev/null @@ -1,15 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.actions { - position: sticky; - bottom: 0; - display: flex; - justify-content: flex-end; - gap: 10px; - margin: 26px -32px -32px; - padding: 16px 32px; - background: $white; - border-top: 1px solid $gray-200; - border-radius: 0 0 $r-lg $r-lg; - box-shadow: 0 -4px 16px rgba(15, 36, 64, .04); -} diff --git a/src/webparts/requestPortal/shared/ui/FormActions/FormActions.tsx b/src/webparts/requestPortal/shared/ui/FormActions/FormActions.tsx deleted file mode 100644 index 49d4f83..0000000 --- a/src/webparts/requestPortal/shared/ui/FormActions/FormActions.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; -import styles from './FormActions.module.scss'; - -interface IFormActionsProps { - children: React.ReactNode; -} - -/** Нижняя липкая панель действий формы («Отмена» / «Сохранить»). */ -export const FormActions: React.FC = ({ children }) => { - return
{children}
; -}; diff --git a/src/webparts/requestPortal/shared/ui/FormCard/FormCard.module.scss b/src/webparts/requestPortal/shared/ui/FormCard/FormCard.module.scss deleted file mode 100644 index 5f17def..0000000 --- a/src/webparts/requestPortal/shared/ui/FormCard/FormCard.module.scss +++ /dev/null @@ -1,42 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.card { - background: $white; - border: 1px solid $gray-200; - border-radius: $r-lg; - box-shadow: $sh-1; - padding: 32px; -} - -.header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} - -.title { - font-family: $font-serif; - font-size: $fs-form-title; - font-weight: 600; - color: $navy-900; -} - -.headerRight { - flex-shrink: 0; - display: flex; - align-items: center; - gap: 8px; -} - -.subtitle { - font-size: 12px; - color: $gray-400; - margin-top: 6px; - letter-spacing: .3px; -} - -.hairline { - @include hairline; - margin: 6px 0 22px; -} diff --git a/src/webparts/requestPortal/shared/ui/FormCard/FormCard.tsx b/src/webparts/requestPortal/shared/ui/FormCard/FormCard.tsx deleted file mode 100644 index de73ce7..0000000 --- a/src/webparts/requestPortal/shared/ui/FormCard/FormCard.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import styles from './FormCard.module.scss'; - -interface IFormCardProps { - title: string; - subtitle?: string; - /** Контент в правом верхнем углу шапки (например, номер заявки). */ - headerRight?: React.ReactNode; - /** Строка под подзаголовком (например, инициатор заявки). */ - belowSubtitle?: React.ReactNode; - children: React.ReactNode; -} - -/** Белая карточка формы: заголовок (Lora) + золотой hairline + содержимое. */ -export const FormCard: React.FC = ({ title, subtitle, headerRight, belowSubtitle, children }) => { - return ( -
-
-
-
{title}
- {subtitle &&
{subtitle}
} - {belowSubtitle} -
- {headerRight &&
{headerRight}
} -
-
- {children} -
- ); -}; diff --git a/src/webparts/requestPortal/shared/ui/FormField/FormField.module.scss b/src/webparts/requestPortal/shared/ui/FormField/FormField.module.scss deleted file mode 100644 index fb9886e..0000000 --- a/src/webparts/requestPortal/shared/ui/FormField/FormField.module.scss +++ /dev/null @@ -1,23 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.field { - display: flex; - flex-direction: column; - gap: 6px; - min-width: 0; -} - -.full { - grid-column: 1 / -1; -} - -.label { - font-size: $fs-label; - font-weight: 500; - color: $gray-700; - letter-spacing: .2px; -} - -.req { - color: $gold-600; -} diff --git a/src/webparts/requestPortal/shared/ui/FormField/FormField.tsx b/src/webparts/requestPortal/shared/ui/FormField/FormField.tsx deleted file mode 100644 index cf07f90..0000000 --- a/src/webparts/requestPortal/shared/ui/FormField/FormField.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import styles from './FormField.module.scss'; - -interface IFormFieldProps { - label: string; - htmlFor?: string; - required?: boolean; - /** Занять обе колонки сетки. */ - full?: boolean; - children: React.ReactNode; -} - -/** Ячейка формы: подпись со звёздочкой для обязательных + контрол. */ -export const FormField: React.FC = ({ label, htmlFor, required, full, children }) => { - const classes = [styles.field, full ? styles.full : ''].filter(Boolean).join(' '); - - return ( -
- - {children} -
- ); -}; diff --git a/src/webparts/requestPortal/shared/ui/FormGrid/FormGrid.module.scss b/src/webparts/requestPortal/shared/ui/FormGrid/FormGrid.module.scss deleted file mode 100644 index a8df5e6..0000000 --- a/src/webparts/requestPortal/shared/ui/FormGrid/FormGrid.module.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 18px 22px; - - @media (max-width: 640px) { - grid-template-columns: 1fr; - } -} diff --git a/src/webparts/requestPortal/shared/ui/FormGrid/FormGrid.tsx b/src/webparts/requestPortal/shared/ui/FormGrid/FormGrid.tsx deleted file mode 100644 index 8132eb0..0000000 --- a/src/webparts/requestPortal/shared/ui/FormGrid/FormGrid.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; -import styles from './FormGrid.module.scss'; - -interface IFormGridProps { - children: React.ReactNode; -} - -/** Сетка полей в 2 колонки. Поля-дети — FormField (full — на всю ширину). */ -export const FormGrid: React.FC = ({ children }) => { - return
{children}
; -}; diff --git a/src/webparts/requestPortal/shared/ui/FormMode.tsx b/src/webparts/requestPortal/shared/ui/FormMode.tsx deleted file mode 100644 index 4b2f492..0000000 --- a/src/webparts/requestPortal/shared/ui/FormMode.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; - -// Режим формы «только чтение». Проставляется один раз оборачиванием -// (...), а поля-примитивы сами его подхватывают — -// не нужно тащить readOnly через каждую секцию и каждое поле. -const FormModeContext = React.createContext(false); - -export const FormModeProvider: React.FC<{ readOnly: boolean; children: React.ReactNode }> = ({ - readOnly, - children -}) => {children}; - -/** true, если форма отрисовывается в режиме «только чтение». */ -export function useFormReadOnly(): boolean { - return React.useContext(FormModeContext); -} diff --git a/src/webparts/requestPortal/shared/ui/Icon/Icon.module.scss b/src/webparts/requestPortal/shared/ui/Icon/Icon.module.scss deleted file mode 100644 index bb45e04..0000000 --- a/src/webparts/requestPortal/shared/ui/Icon/Icon.module.scss +++ /dev/null @@ -1,22 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.icon { - width: 20px; - height: 20px; - flex-shrink: 0; - fill: none; - stroke: currentColor; - stroke-width: 1.5; - stroke-linecap: round; - stroke-linejoin: round; -} - -.sm { - width: 16px; - height: 16px; -} - -.lg { - width: 24px; - height: 24px; -} diff --git a/src/webparts/requestPortal/shared/ui/Icon/Icon.tsx b/src/webparts/requestPortal/shared/ui/Icon/Icon.tsx deleted file mode 100644 index 1c835dd..0000000 --- a/src/webparts/requestPortal/shared/ui/Icon/Icon.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import styles from './Icon.module.scss'; -import { IconName } from './iconNames'; - -type IconSize = 'sm' | 'md' | 'lg'; - -interface IIconProps { - name: IconName; - size?: IconSize; - className?: string; -} - -const sizeClass: Record = { - sm: styles.sm, - md: '', - lg: styles.lg -}; - -/** - * Иконка из общего спрайта. Спрайт (IconSprite) должен быть отрисован - * один раз в корне портала, иначе ссылки #i- ничего не покажут. - */ -export const Icon: React.FC = ({ name, size = 'md', className }) => { - const classes = [styles.icon, sizeClass[size], className].filter(Boolean).join(' '); - return ( - - ); -}; diff --git a/src/webparts/requestPortal/shared/ui/Icon/IconSprite.tsx b/src/webparts/requestPortal/shared/ui/Icon/IconSprite.tsx deleted file mode 100644 index 247f420..0000000 --- a/src/webparts/requestPortal/shared/ui/Icon/IconSprite.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import * as React from 'react'; - -/** - * Скрытый SVG-спрайт со всеми иконками портала. - * Рендерится один раз в корне (RequestPortal). Компонент Icon ссылается - * на символы по id `#i-` (см. iconNames.ts). - */ -export const IconSprite: React.FC = () => ( - -); diff --git a/src/webparts/requestPortal/shared/ui/Icon/iconNames.ts b/src/webparts/requestPortal/shared/ui/Icon/iconNames.ts deleted file mode 100644 index 3ae8c48..0000000 --- a/src/webparts/requestPortal/shared/ui/Icon/iconNames.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Единый набор имён иконок. Значение = id символа в спрайте (см. IconSprite). -// Добавляя иконку: 1) добавь имя сюда, 2) добавь в IconSprite. -export const ICON_NAMES = [ - 'building', - 'chart', - 'monitor', - 'calculator', - 'helmet', - 'user-check', - 'send', - 'download', - 'upload', - 'folder', - 'document', - 'calendar', - 'clipboard', - 'people', - 'vehicle', - 'chat', - 'camera', - 'image', - 'edit', - 'copy', - 'more', - 'trash', - 'close', - 'chevron-right', - 'chevron-down', - 'chevron-left', - 'arrow-left', - 'search', - 'warning', - 'check', - 'plus' -] as const; - -export type IconName = (typeof ICON_NAMES)[number]; diff --git a/src/webparts/requestPortal/shared/ui/ItemsTable/ItemsTable.tsx b/src/webparts/requestPortal/shared/ui/ItemsTable/ItemsTable.tsx index 7c2a0f1..105b859 100644 --- a/src/webparts/requestPortal/shared/ui/ItemsTable/ItemsTable.tsx +++ b/src/webparts/requestPortal/shared/ui/ItemsTable/ItemsTable.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import styles from './ItemsTable.module.scss'; -import { Button } from '../Button/Button'; -import { Icon } from '../Icon/Icon'; -import { useFormReadOnly } from '../FormMode'; +import { Button, Icon } from '@tnsu/ui-kit'; +import { useFormReadOnly } from '@tnsu/ui-kit'; import { ILineItem, EMPTY_LINE_ITEM } from './ILineItem'; import { parseExcelRows } from './parseExcelRows'; diff --git a/src/webparts/requestPortal/shared/ui/MoneyInput/MoneyInput.tsx b/src/webparts/requestPortal/shared/ui/MoneyInput/MoneyInput.tsx deleted file mode 100644 index 2a1ec32..0000000 --- a/src/webparts/requestPortal/shared/ui/MoneyInput/MoneyInput.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { TextInput } from '../TextInput/TextInput'; - -// Показ с разделителем тысяч пробелом: 350000 → «350 000». -function formatThousands(input: string): string { - const digits = input.replace(/\D/g, ''); - if (!digits) { - return ''; - } - return digits.replace(/\B(?=(\d{3})+(?!\d))/g, ' '); -} - -interface IMoneyInputProps { - id?: string; - value: string; - onChange: (value: string) => void; - readOnly?: boolean; -} - -/** - * Ввод суммы с разделителем тысяч. В модель формы кладём «сырые» цифры - * (без пробелов), поэтому запись в числовую колонку SP работает как есть. - */ -export const MoneyInput: React.FC = ({ id, value, onChange, readOnly }) => ( - onChange(v.replace(/\D/g, ''))} - readOnly={readOnly} - /> -); diff --git a/src/webparts/requestPortal/shared/ui/PeopleField/PeopleField.tsx b/src/webparts/requestPortal/shared/ui/PeopleField/PeopleField.tsx index 30ab60b..256411b 100644 --- a/src/webparts/requestPortal/shared/ui/PeopleField/PeopleField.tsx +++ b/src/webparts/requestPortal/shared/ui/PeopleField/PeopleField.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { PeoplePicker, PrincipalType } from '@pnp/spfx-controls-react/lib/PeoplePicker'; import { WebPartContext } from '@microsoft/sp-webpart-base'; import { buildPickerContext, pickedLogin, IPickedUser } from './pickerContext'; -import { useFormReadOnly } from '../FormMode'; +import { useFormReadOnly } from '@tnsu/ui-kit'; interface IPeopleFieldProps { context: WebPartContext; diff --git a/src/webparts/requestPortal/shared/ui/PeopleField/PeopleFieldMulti.tsx b/src/webparts/requestPortal/shared/ui/PeopleField/PeopleFieldMulti.tsx index 4a56a1b..c51f1c0 100644 --- a/src/webparts/requestPortal/shared/ui/PeopleField/PeopleFieldMulti.tsx +++ b/src/webparts/requestPortal/shared/ui/PeopleField/PeopleFieldMulti.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { PeoplePicker, PrincipalType } from '@pnp/spfx-controls-react/lib/PeoplePicker'; import { WebPartContext } from '@microsoft/sp-webpart-base'; import { buildPickerContext, pickedLogin, IPickedUser } from './pickerContext'; -import { useFormReadOnly } from '../FormMode'; +import { useFormReadOnly } from '@tnsu/ui-kit'; interface IPeopleFieldMultiProps { context: WebPartContext; diff --git a/src/webparts/requestPortal/shared/ui/PhoneInput/PhoneInput.tsx b/src/webparts/requestPortal/shared/ui/PhoneInput/PhoneInput.tsx deleted file mode 100644 index 55254e3..0000000 --- a/src/webparts/requestPortal/shared/ui/PhoneInput/PhoneInput.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react'; -import { TextInput } from '../TextInput/TextInput'; - -// Маска номера Казахстана: +7 (XXX) XXX-XX-XX. Идемпотентна — можно применять -// и к уже отформатированному значению (напр. загруженному из SharePoint). -function formatKzPhone(input: string): string { - let digits = input.replace(/\D/g, ''); - if (digits.length === 0) { - return ''; - } - if (digits[0] === '8') { - digits = '7' + digits.slice(1); - } - if (digits[0] !== '7') { - digits = '7' + digits; - } - digits = digits.slice(0, 11); // код страны + 10 цифр - - const rest = digits.slice(1); - let out = '+7'; - if (rest.length > 0) out += ' (' + rest.slice(0, 3); - if (rest.length >= 3) out += ')'; - if (rest.length > 3) out += ' ' + rest.slice(3, 6); - if (rest.length > 6) out += '-' + rest.slice(6, 8); - if (rest.length > 8) out += '-' + rest.slice(8, 10); - return out; -} - -interface IPhoneInputProps { - id?: string; - value: string; - onChange: (value: string) => void; - readOnly?: boolean; -} - -/** Ввод телефона с маской +7 (XXX) XXX-XX-XX. */ -export const PhoneInput: React.FC = ({ id, value, onChange, readOnly }) => ( - onChange(formatKzPhone(v))} - placeholder="+7 (___) ___-__-__" - readOnly={readOnly} - /> -); diff --git a/src/webparts/requestPortal/shared/ui/PrintButton/PrintButton.tsx b/src/webparts/requestPortal/shared/ui/PrintButton/PrintButton.tsx deleted file mode 100644 index 8910f02..0000000 --- a/src/webparts/requestPortal/shared/ui/PrintButton/PrintButton.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import * as React from 'react'; -import { Button } from '../Button/Button'; -import { Icon } from '../Icon/Icon'; - -// Значение поля для печати. Дату (input type=date, ISO yyyy-mm-dd) — в дд.мм.гггг. -function formatValue(el: HTMLInputElement): string { - if (el.type === 'date' && /^\d{4}-\d{2}-\d{2}$/.test(el.value)) { - const [y, m, d] = el.value.split('-'); - return `${d}.${m}.${y}`; - } - return el.value; -} - -// Печатаем как ДАННЫЕ, а не как форму: каждое поле ввода в клоне заменяем на -// обычный текст его значения (значения берём из живого DOM — React держит их как -// свойства, в клонированном HTML их нет). Так textarea/поля не занимают лишнего -// места и лист выглядит как справка, а не как незаполненный бланк. -function renderValuesAsText(source: Element, clone: Element): void { - const put = (cloneEl: Element | undefined, text: string): void => { - if (!cloneEl) return; - const div = document.createElement('div'); - div.className = 'rp-print-value'; - div.textContent = text && text.length > 0 ? text : '—'; - (cloneEl.closest('label') ?? cloneEl).replaceWith(div); - }; - - const cInputs = clone.querySelectorAll('input'); - source.querySelectorAll('input').forEach((el, i) => { - if (el.type === 'checkbox' || el.type === 'radio') { - put(cInputs[i], el.checked ? 'Да' : 'Нет'); - } else { - put(cInputs[i], formatValue(el)); - } - }); - - const cAreas = clone.querySelectorAll('textarea'); - source.querySelectorAll('textarea').forEach((el, i) => put(cAreas[i], el.value)); - - const cSelects = clone.querySelectorAll('select'); - source.querySelectorAll('select').forEach((el, i) => { - const opt = el.options[el.selectedIndex]; - put(cSelects[i], opt && opt.value ? opt.text : ''); - }); -} - -// Стили печатного окна: чистый лист (без «коробки» карточки), без иконок, -// значения — обычным текстом с переносами. -const PRINT_CSS = - // height:auto перебивает глобальный CSS SharePoint (html/body 100vh) — иначе - // body раздувается выше листа A4 и в печать идёт лишняя пустая страница. - 'html,body{margin:0;background:#fff;height:auto!important;min-height:0!important}' + - 'body{padding:24px;font-size:13px}' + - 'svg{display:none!important}' + - // Фото сотрудника (аватар персоны PeoplePicker) в печать не идёт — на бумаге - // нужны только ФИО и e-mail. Прячем весь круг-аватар, чтобы не оставался - // пустой кружок. Текст персоны (имя/почта) остаётся. - '.ms-Persona-imageArea,.ms-Persona-image{display:none!important}' + - '.rp-print-root>*{border:0!important;box-shadow:none!important;padding:0!important}' + - '.rp-print-value{white-space:pre-wrap;color:#000;padding:1px 0 2px}'; - -/** - * Кнопка «Печать»: переносит документ (`.rp-print-root`) в отдельное чистое окно, - * заменяя поля ввода на текст значений, и печатает его. Не зависит от скролл- - * контейнеров/высоты SharePoint, корректно разбивается на страницы. Общая для форм. - */ -export const PrintButton: React.FC<{ label?: string }> = ({ label = 'Печать' }) => { - const onPrint = (): void => { - const source = document.querySelector('.rp-print-root'); - if (!source) { - window.print(); - return; - } - const win = window.open('', '_blank', 'width=980,height=1200'); - if (!win) { - window.print(); // всплывающее окно заблокировано — печатаем как есть - return; - } - - const clone = source.cloneNode(true) as Element; - // Убираем из печати всё помеченное rp-no-print (напр. блок «Вложения»). - clone.querySelectorAll('.rp-no-print').forEach((el) => el.remove()); - renderValuesAsText(source, clone); - - // Переносим стили страницы (CSS-модули инжектятся как ${clone.outerHTML}` - ); - win.document.close(); - win.focus(); - - // Даём стилям/шрифтам примениться, затем печатаем и закрываем окно. - const done = (): void => { - win.print(); - win.close(); - }; - if (win.document.readyState === 'complete') { - window.setTimeout(done, 350); - } else { - win.onload = (): void => { - window.setTimeout(done, 350); - }; - } - }; - - return ( - - ); -}; diff --git a/src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.module.scss b/src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.module.scss deleted file mode 100644 index f37d77d..0000000 --- a/src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.module.scss +++ /dev/null @@ -1,16 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.badge { - display: inline-flex; - align-items: center; - padding: 5px 12px; - border: 1px solid $gray-200; - border-radius: $r-sm; - background: $navy-50; - color: $navy-800; - font-family: $font-sans; - font-size: 13px; - font-weight: 600; - letter-spacing: .3px; - white-space: nowrap; -} diff --git a/src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.tsx b/src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.tsx deleted file mode 100644 index 1886ff1..0000000 --- a/src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react'; -import styles from './RequestNumber.module.scss'; - -/** Уникальный код заявки ({requestTypeId}-{YY}{MM}-{itemId}) — плашка в правом углу шапки формы. */ -export const RequestNumber: React.FC<{ code: string }> = ({ code }) => ( - №{code} -); diff --git a/src/webparts/requestPortal/shared/ui/Select/Select.module.scss b/src/webparts/requestPortal/shared/ui/Select/Select.module.scss deleted file mode 100644 index 647e71b..0000000 --- a/src/webparts/requestPortal/shared/ui/Select/Select.module.scss +++ /dev/null @@ -1,20 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.select { - appearance: none; - padding: 11px 36px 11px 13px; - border: 1px solid $gray-300; - border-radius: $r-sm; - background-color: $white; - background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%236B7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 12px center; - font-family: $font-sans; - font-size: 13.5px; - color: $gray-900; - cursor: pointer; - transition: .18s; - - @include input-focus-gold; - @include readonly-field(':disabled'); -} diff --git a/src/webparts/requestPortal/shared/ui/Select/Select.tsx b/src/webparts/requestPortal/shared/ui/Select/Select.tsx deleted file mode 100644 index c77e9fa..0000000 --- a/src/webparts/requestPortal/shared/ui/Select/Select.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import styles from './Select.module.scss'; -import { useFormReadOnly } from '../FormMode'; - -export interface ISelectOption { - value: string; - label: string; -} - -interface ISelectProps { - id?: string; - value: string; - onChange: (value: string) => void; - options: ISelectOption[]; - placeholder?: string; - /** Только чтение (режим согласующего). У select нет readonly — используем disabled. */ - readOnly?: boolean; -} - -/** Выпадающий список (SharePoint Choice). */ -export const Select: React.FC = ({ id, value, onChange, options, placeholder, readOnly }) => { - const contextReadOnly = useFormReadOnly(); - const effectiveReadOnly = readOnly ?? contextReadOnly; - return ( - - ); -}; diff --git a/src/webparts/requestPortal/shared/ui/SubmitActions/SubmitActions.tsx b/src/webparts/requestPortal/shared/ui/SubmitActions/SubmitActions.tsx index fc549fa..4e05a52 100644 --- a/src/webparts/requestPortal/shared/ui/SubmitActions/SubmitActions.tsx +++ b/src/webparts/requestPortal/shared/ui/SubmitActions/SubmitActions.tsx @@ -1,7 +1,5 @@ import * as React from 'react'; -import { Button } from '@tnsu/ui-kit'; -import { Icon } from '../Icon/Icon'; -import { FormActions } from '../FormActions/FormActions'; +import { Button, Icon, FormActions } from '@tnsu/ui-kit'; import { IRequestSubmit } from '../useRequestSubmit'; export interface ISubmitActionsProps { diff --git a/src/webparts/requestPortal/shared/ui/TextArea/TextArea.module.scss b/src/webparts/requestPortal/shared/ui/TextArea/TextArea.module.scss deleted file mode 100644 index 2fd1ae5..0000000 --- a/src/webparts/requestPortal/shared/ui/TextArea/TextArea.module.scss +++ /dev/null @@ -1,21 +0,0 @@ -@import '../../../../../styles/design-tokens'; - -.textarea { - padding: 11px 13px; - border: 1px solid $gray-300; - border-radius: $r-sm; - background: $white; - font-family: $font-sans; - font-size: 13.5px; - color: $gray-900; - resize: vertical; - min-height: 80px; - transition: .18s; - - @include input-focus-gold; - @include readonly-field(':read-only'); - - &::placeholder { - color: $gray-400; - } -} diff --git a/src/webparts/requestPortal/shared/ui/TextArea/TextArea.tsx b/src/webparts/requestPortal/shared/ui/TextArea/TextArea.tsx deleted file mode 100644 index abdb5ee..0000000 --- a/src/webparts/requestPortal/shared/ui/TextArea/TextArea.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import styles from './TextArea.module.scss'; -import { useFormReadOnly } from '../FormMode'; - -interface ITextAreaProps { - id?: string; - value: string; - onChange: (value: string) => void; - placeholder?: string; - rows?: number; - /** Только чтение (режим согласующего). */ - readOnly?: boolean; -} - -/** Многострочный ввод (SharePoint Note). */ -export const TextArea: React.FC = ({ id, value, onChange, placeholder, rows = 3, readOnly }) => { - const contextReadOnly = useFormReadOnly(); - const effectiveReadOnly = readOnly ?? contextReadOnly; - return ( -