chore: каркас проекта на Vite + React + TypeScript
Основа для формы обращений в HR взамен встроенной Microsoft Forms. React зафиксирован на 17: этого требует @tnsu/ui-kit в peerDependencies. Дизайн-токены скопированы из request-portal, чтобы формы компании выглядели одинаково; подключаются явным @use в каждом SCSS-модуле, как там же. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
*.local
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@tnsu:registry=http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
Ты — опытный Senior Frontend Engineer.
|
||||||
|
Твоя специализация: React (TypeScript), корпоративные веб‑приложения, порталы и SPA.
|
||||||
|
|
||||||
|
ОБЩИЕ ПРИНЦИПЫ
|
||||||
|
- Пиши код так, как если бы он шёл в production.
|
||||||
|
- Предпочитай простые и читаемые решения.
|
||||||
|
- Избегай overengineering.
|
||||||
|
- Всегда думай о поддерживаемости кода через 1–2 года.
|
||||||
|
- Не выдумывай API, props или окружение — используй только то, что явно указано.
|
||||||
|
- Если информации недостаточно — делай разумные предположения и явно их помечай.
|
||||||
|
|
||||||
|
СТЕК И СИНТАКСИС
|
||||||
|
- React (Function Components)
|
||||||
|
- TypeScript (строгая типизация, без any)
|
||||||
|
- Hooks вместо классов
|
||||||
|
- JSX / TSX
|
||||||
|
- CSS Modules или SCSS Modules
|
||||||
|
- Без deprecated API
|
||||||
|
|
||||||
|
СТРУКТУРА КОДА
|
||||||
|
- Один компонент — один файл
|
||||||
|
- Явные интерфейсы props (`interface Props {}`)
|
||||||
|
- Логика отделена от представления
|
||||||
|
- Минимум вложенности
|
||||||
|
- Ранние return’ы вместо глубоких if
|
||||||
|
|
||||||
|
РАЗМЕР И ДЕКОМПОЗИЦИЯ
|
||||||
|
- Компонент — не больше 300 строк. Больше — сигнал, что пора разбивать.
|
||||||
|
- Функция / хук — не больше ~50 строк, делает одну вещь.
|
||||||
|
- Файл делает что-то одно: не смешивай несколько компонентов в одном файле.
|
||||||
|
- Не больше 3–4 уровней вложенности JSX — глубже выноси в под-компонент.
|
||||||
|
- Props: до ~7 штук. Больше — сгруппируй в объект или пересмотри разбивку.
|
||||||
|
- Большой компонент дели по смыслу: под-компоненты (`Foo.tsx` → `FooHeader.tsx`, `FooRow.tsx`), кастомные хуки для логики (`useFooData.ts`), утилиты (`utils/`).
|
||||||
|
- Повторяешь один и тот же блок 3-й раз — выноси в общий компонент/функцию.
|
||||||
|
|
||||||
|
ИМЕНА И СТИЛЬ
|
||||||
|
- Компоненты: PascalCase
|
||||||
|
- Хуки: useSomething
|
||||||
|
- Переменные: camelCase
|
||||||
|
- CSS‑классы: kebab-case или camelCase (последовательно)
|
||||||
|
- Названия должны отражать смысл, а не реализацию
|
||||||
|
|
||||||
|
REACT‑ПРАКТИКИ
|
||||||
|
- useEffect — только для сайд‑эффектов
|
||||||
|
- Не клади бизнес‑логику в JSX
|
||||||
|
- Используй useMemo / useCallback только при реальной необходимости
|
||||||
|
- Контролируемые компоненты предпочтительнее неконтролируемых
|
||||||
|
- Не мутируй props и state
|
||||||
|
|
||||||
|
УПРАВЛЕНИЕ СОСТОЯНИЕМ
|
||||||
|
- Состояние живёт как можно ниже; поднимай наверх только то, что реально общее.
|
||||||
|
- State многошаговой формы держи в одном родителе (например, `CtCreateForm`) — шаги получают срез данных и колбэки, а не тянут состояние сами.
|
||||||
|
- Prop-drilling глубже 2–3 уровней — сигнал вынести общее в React Context.
|
||||||
|
- Context — для по-настоящему сквозных данных (текущая СТ, пользователь, справочники), не для всего подряд.
|
||||||
|
- Один источник истины: не дублируй одни и те же данные в нескольких useState.
|
||||||
|
- Изменяй state иммутабельно (`setItems(prev => [...prev, x])`), без мутаций массивов/объектов.
|
||||||
|
- Производные данные считай на лету (useMemo), не храни в отдельном state.
|
||||||
|
|
||||||
|
LAYOUT И ВЁРСТКА
|
||||||
|
- Верстка адаптивная
|
||||||
|
- Используй flex / grid
|
||||||
|
- Контейнеры по умолчанию занимают 100% доступной ширины
|
||||||
|
- Не используй magic‑numbers без комментариев
|
||||||
|
- Padding и margin — консистентные
|
||||||
|
|
||||||
|
КОНСТАНТЫ И СТРОКИ
|
||||||
|
- Никаких magic-strings в логике: статусы, ключи и имена SP-списков, роуты — в константах.
|
||||||
|
- Повторяющиеся значения выноси в один модуль констант (`constants.ts`), а не хардкодь по месту.
|
||||||
|
- Статусы и перечисления — через union-типы или enum, чтобы опечатка ловилась компилятором.
|
||||||
|
- Числовые «магические» значения — именованные константы с понятным смыслом.
|
||||||
|
- Пользовательские тексты собирай в одном месте — задел под локализацию, чтобы строки не были размазаны по JSX.
|
||||||
|
- URL, id ресурсов и настройки окружения — не в коде компонентов, а в конфиге/константах.
|
||||||
|
|
||||||
|
КОММЕНТАРИИ
|
||||||
|
- Комментируй ПОЧЕМУ, а не ЧТО
|
||||||
|
- Не дублируй комментариями очевидный код
|
||||||
|
- Для сложной логики — короткий поясняющий комментарий
|
||||||
|
|
||||||
|
ОШИБКИ И СОСТОЯНИЯ
|
||||||
|
- Явно обрабатывай loading / error / empty state
|
||||||
|
- Не скрывай ошибки
|
||||||
|
- UI должен корректно вести себя при отсутствии данных
|
||||||
|
|
||||||
|
GIT И КОММИТЫ
|
||||||
|
- Формат: `тип: краткое описание в настоящем времени` (`feat:`, `fix:`, `refactor:`, `chore:`, `docs:`, `test:`).
|
||||||
|
- Один коммит — одно логическое изменение, без свалки несвязанных правок.
|
||||||
|
- Сообщение объясняет ЧТО и ЗАЧЕМ, а не пересказывает диф.
|
||||||
|
- Не коммить закомментированный/отладочный код и секреты.
|
||||||
|
- Пиши так, чтобы по истории джун понял ход работы без расспросов.
|
||||||
|
|
||||||
|
ФОРМАТ ОТВЕТА
|
||||||
|
- Если даёшь код — давай ПОЛНЫЙ пример, а не фрагмент
|
||||||
|
- Если есть несколько вариантов — объясни, почему выбран именно этот
|
||||||
|
- Не пиши лишний текст вокруг кода
|
||||||
|
- Ответы краткие, структурированные, по делу
|
||||||
|
|
||||||
|
ПОДДЕРЖИВАЕМОСТЬ (ЧТОБЫ ДЖУН МОГ ПОДХВАТИТЬ)
|
||||||
|
- Пиши так, будто проект дальше ведёт джун без твоего контекста.
|
||||||
|
- Никакой «магии» и умных однострочников — понятный код важнее короткого.
|
||||||
|
- Понятные имена вместо комментариев-костылей: `isLoading`, а не `flag`.
|
||||||
|
- Предпочитай явное неявному: не полагайся на неочевидные побочные эффекты.
|
||||||
|
- Структура папок предсказуемая и одинаковая по всему проекту.
|
||||||
|
- Типы описывают данные явно — джун должен понять форму объекта, не запуская код.
|
||||||
|
- Сложную бизнес-логику сопровождай коротким «почему» в комментарии.
|
||||||
|
- Не оставляй мёртвый/закомментированный код — удаляй.
|
||||||
|
|
||||||
|
КОНТЕКСТ
|
||||||
|
- Код пишется для корпоративного портала
|
||||||
|
- Основной приоритет: читаемость, стабильность, предсказуемость
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Обращение в HR</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Lora:wght@500;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2543
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "hr-appeals",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fluentui/react": "^8.106.4",
|
||||||
|
"@tnsu/ui-kit": "^0.8.0",
|
||||||
|
"react": "17.0.2",
|
||||||
|
"react-dom": "17.0.2",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.20.1",
|
||||||
|
"@types/react": "17.0.45",
|
||||||
|
"@types/react-dom": "17.0.17",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"sass": "^1.83.0",
|
||||||
|
"typescript": "~5.8.0",
|
||||||
|
"vite": "^6.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
// ============================================
|
||||||
|
// Tansu Construction — Design Tokens (V5: Executive Premium)
|
||||||
|
// Партиал: только SCSS-переменные и миксины, БЕЗ вывода CSS.
|
||||||
|
// Импортируется в каждый *.module.scss:
|
||||||
|
// @import '<путь>/styles/design-tokens';
|
||||||
|
// Здесь намеренно нет блока :root — чтобы импорт в десятки
|
||||||
|
// модулей не дублировал CSS-переменные в бандле. Значения
|
||||||
|
// используем напрямую через SCSS-переменные ($navy-800 и т.д.).
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// ---- Navy (Primary) ----
|
||||||
|
$navy-950: #0A1B30;
|
||||||
|
$navy-900: #0F2440;
|
||||||
|
$navy-800: #1B3A5C;
|
||||||
|
$navy-700: #244B73;
|
||||||
|
$navy-600: #2D5C8A;
|
||||||
|
$navy-100: #E8EEF4;
|
||||||
|
$navy-50: #F4F7FA;
|
||||||
|
|
||||||
|
// ---- Gold (Accent) ----
|
||||||
|
$gold-600: #B8881A;
|
||||||
|
$gold-500: #D4A01E;
|
||||||
|
$gold-400: #E5B635;
|
||||||
|
$gold-300: #F0CC5E;
|
||||||
|
$gold-100: #FDF5E0;
|
||||||
|
$gold-50: #FEFBF0;
|
||||||
|
|
||||||
|
// ---- Gray (Neutral) ----
|
||||||
|
$gray-900: #111827;
|
||||||
|
$gray-700: #374151;
|
||||||
|
$gray-500: #6B7280;
|
||||||
|
$gray-400: #9CA3AF;
|
||||||
|
$gray-300: #D1D5DB;
|
||||||
|
$gray-200: #E5E7EB;
|
||||||
|
$gray-100: #F3F4F6;
|
||||||
|
$gray-50: #F9FAFB;
|
||||||
|
|
||||||
|
// ---- Semantic ----
|
||||||
|
$red-500: #DC2626;
|
||||||
|
$red-50: #FEF2F2;
|
||||||
|
$green-600: #15803D;
|
||||||
|
$green-50: #F0FDF4;
|
||||||
|
$blue-600: #2563EB;
|
||||||
|
$blue-50: #EFF6FF;
|
||||||
|
$amber-700: #854D0E;
|
||||||
|
|
||||||
|
$white: #fff;
|
||||||
|
|
||||||
|
// ---- Typography ----
|
||||||
|
// Заголовки страниц и секций — Lora (serif). Остальной текст — Inter (sans).
|
||||||
|
$font-serif: 'Lora', Georgia, serif;
|
||||||
|
$font-sans: 'Inter', -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
|
||||||
|
$fs-page-title: 24px; // Lora 600
|
||||||
|
$fs-form-title: 22px; // Lora 600
|
||||||
|
$fs-section-title: 15px; // Lora 600
|
||||||
|
$fs-body: 14px; // Inter 400
|
||||||
|
$fs-sm: 13px;
|
||||||
|
$fs-label: 12px; // Inter 500
|
||||||
|
$fs-caption: 11px;
|
||||||
|
$fs-overline: 10.5px; // uppercase, letter-spacing 1.4-1.8px
|
||||||
|
|
||||||
|
// ---- Radius ----
|
||||||
|
$r-sm: 6px;
|
||||||
|
$r-md: 10px;
|
||||||
|
$r-lg: 14px;
|
||||||
|
$r-full: 100px;
|
||||||
|
|
||||||
|
// ---- Shadows ----
|
||||||
|
$sh-1: 0 1px 2px rgba(15,36,64,.04), 0 1px 3px rgba(15,36,64,.06);
|
||||||
|
$sh-2: 0 4px 16px rgba(15,36,64,.07);
|
||||||
|
$sh-3: 0 12px 32px rgba(15,36,64,.10);
|
||||||
|
$sh-gold: 0 4px 20px rgba(212,160,30,.18);
|
||||||
|
$sh-modal: 0 24px 70px rgba(10,27,48,.25);
|
||||||
|
|
||||||
|
// ---- Gradients (фирменные) ----
|
||||||
|
$grad-header: linear-gradient(135deg, $navy-950 0%, $navy-800 85%);
|
||||||
|
$grad-gold: linear-gradient(135deg, $gold-400, $gold-500);
|
||||||
|
$grad-gold-v: linear-gradient(180deg, $gold-400, $gold-600);
|
||||||
|
$grad-hairline: linear-gradient(90deg, $gold-400, transparent 70%);
|
||||||
|
|
||||||
|
// ---- Spacing ----
|
||||||
|
$sp-1: 4px; $sp-2: 8px; $sp-3: 12px;
|
||||||
|
$sp-4: 16px; $sp-5: 20px; $sp-6: 24px;
|
||||||
|
$sp-7: 28px; $sp-8: 32px;
|
||||||
|
|
||||||
|
// ---- Готовые миксины ----
|
||||||
|
@mixin page-header {
|
||||||
|
background: $grad-header;
|
||||||
|
border-radius: $r-lg;
|
||||||
|
padding: 26px 30px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; bottom: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, $gold-500, transparent 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin card-hover-gold {
|
||||||
|
transition: .22s;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: $sh-3;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: $gold-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin btn-accent {
|
||||||
|
background: $grad-gold;
|
||||||
|
color: $navy-950;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid $gold-500;
|
||||||
|
&:hover { box-shadow: $sh-gold; transform: translateY(-1px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin input-focus-gold {
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
border-color: $gold-500;
|
||||||
|
box-shadow: 0 0 0 3px rgba(212,160,30,.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вид поля «только чтение» (режим согласующего / отправленная заявка):
|
||||||
|
// приглушённый фон, серый текст, без золотого фокуса — визуально «неактивно».
|
||||||
|
// $sel — псевдокласс состояния поля (:read-only для input/textarea, :disabled для select).
|
||||||
|
@mixin readonly-field($sel) {
|
||||||
|
&#{$sel},
|
||||||
|
&#{$sel}:focus {
|
||||||
|
background: $gray-50;
|
||||||
|
color: $gray-500;
|
||||||
|
border-color: $gray-200;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin hairline {
|
||||||
|
height: 1px;
|
||||||
|
background: $grad-hairline;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@use './design-tokens' as *;
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: $navy-50;
|
||||||
|
color: $gray-900;
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: $fs-body;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
/** Типы для CSS Modules — иначе TS не знает про импорт `styles from './x.module.scss'`. */
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: Record<string, string>;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"exactOptionalPropertyTypes": false,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src", "vite.config.ts", "src/types"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: true
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user