feat(frontend): migrate HomePage and SearchBar to design system components

- HomePage: replace inline styles with <Heading>, <Text>, <Box>
- SearchBar: replace <input> with <TextField>, <ul> with <Surface>,
  <li> with <Box>+<Text>+<Chip>, hardcoded hex with theme tokens
- ESLint no-restricted-imports respected via @mui/material/Box deep import
- Logic unchanged, all 111 tests pass
This commit is contained in:
Sergey Krylov 2026-06-21 16:57:19 +03:00
parent 3534ec4f77
commit bd33412691
5 changed files with 187 additions and 67 deletions

View File

@ -1,14 +1,15 @@
import Box from '@mui/material/Box';
import { Heading, Text } from '@moex-vibe/design-system';
export function HomePage() { export function HomePage() {
return ( return (
<div style={{ textAlign: 'center', paddingTop: 120 }}> <Box textAlign="center" pt={30}>
<h1 style={{ fontSize: 32, fontWeight: 700, marginBottom: 12 }}>MoexVibe</h1> <Heading level={1} size="display">
<p style={{ color: 'var(--color-text-secondary)', fontSize: 16, marginBottom: 32 }}> MoexVibe
Анализ акций и облигаций Московской биржи </Heading>
</p> <Text tone="secondary">Анализ акций и облигаций Московской биржи</Text>
<p style={{ color: '#888', fontSize: 13 }}>Введите название или тикер в строку поиска выше</p> <Text tone="muted">Введите название или тикер в строку поиска выше</Text>
<p style={{ color: '#aaa', fontSize: 12, marginTop: 8 }}> <Text tone="muted">Данные задерживаются на 15 минут · Бесплатный API MOEX ISS</Text>
Данные задерживаются на 15 минут &middot; Бесплатный API MOEX ISS </Box>
</p>
</div>
); );
} }

View File

@ -1,5 +1,7 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
import { Chip, Surface, Text, TextField } from '@moex-vibe/design-system';
import { useSearch } from '@/entities/search'; import { useSearch } from '@/entities/search';
export function SearchBar() { export function SearchBar() {
@ -32,82 +34,81 @@ export function SearchBar() {
const showResults = open && debounced.length >= 2; const showResults = open && debounced.length >= 2;
return ( return (
<div ref={ref} style={{ position: 'relative', width: 400, maxWidth: '100%' }}> <Box ref={ref} sx={{ position: 'relative', width: 400, maxWidth: '100%' }}>
<input <TextField
type="text" label="Поиск"
placeholder="Поиск акций и облигаций..." placeholder="Поиск акций и облигаций..."
hiddenLabel
value={query} value={query}
onChange={(e) => { onChange={(e) => {
setQuery(e.target.value); setQuery(e.target.value);
setOpen(true); setOpen(true);
}} }}
onFocus={() => setOpen(true)} onFocus={() => setOpen(true)}
style={{
width: '100%',
padding: '8px 12px',
border: '1px solid #ccc',
borderRadius: 6,
fontSize: 14,
}}
/> />
{showResults && ( {showResults && (
<ul <Box
style={{ sx={{
position: 'absolute', position: 'absolute',
top: '100%', top: '100%',
left: 0, left: 0,
right: 0, right: 0,
background: '#fff', mt: 0.5,
border: '1px solid #ddd',
borderRadius: 6,
marginTop: 4,
padding: 0,
listStyle: 'none',
zIndex: 100, zIndex: 100,
maxHeight: 360, maxHeight: 360,
overflowY: 'auto', overflowY: 'auto',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
}} }}
> >
{isLoading && <li style={{ padding: 12, color: '#888' }}>Загрузка...</li>} <Surface elevation="sm" padding="none">
{!isLoading && results && results.length === 0 && ( {isLoading && (
<li style={{ padding: 12, color: '#888' }}>Ничего не найдено</li> <Box sx={{ p: 1.5 }}>
)} <Text tone="muted">Загрузка...</Text>
{!isLoading && </Box>
results?.map((item) => ( )}
<li {!isLoading && results && results.length === 0 && (
key={item.secid} <Box sx={{ p: 1.5 }}>
onClick={() => { <Text tone="muted">Ничего не найдено</Text>
setOpen(false); </Box>
setQuery(''); )}
navigate( {!isLoading &&
item.type === 'share' ? `/stocks/${item.secid}` : `/bonds/${item.secid}`, results?.map((item) => (
); <Box
}} key={item.secid}
style={{ onClick={() => {
padding: '10px 12px', setOpen(false);
cursor: 'pointer', setQuery('');
borderBottom: '1px solid #f0f0f0', navigate(
display: 'flex', item.type === 'share' ? `/stocks/${item.secid}` : `/bonds/${item.secid}`,
justifyContent: 'space-between', );
alignItems: 'center', }}
}} sx={{
onMouseEnter={(e) => (e.currentTarget.style.background = '#f5f5f5')} display: 'flex',
onMouseLeave={(e) => (e.currentTarget.style.background = '')} justifyContent: 'space-between',
> alignItems: 'center',
<span> px: 1.5,
<strong>{item.shortName}</strong> py: 1.25,
<span style={{ marginLeft: 8, color: '#888', fontSize: 12 }}>{item.secid}</span> cursor: 'pointer',
</span> borderBottom: 1,
<span borderColor: 'divider',
style={{ fontSize: 12, color: item.type === 'share' ? '#1976d2' : '#2e7d32' }} '&:hover': { bgcolor: 'action.hover' },
'&:last-child': { borderBottom: 0 },
}}
> >
{item.type === 'share' ? 'Акция' : 'Облигация'} <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
</span> <Text>{item.shortName}</Text>
</li> <Text variant="caption" tone="muted">
))} {item.secid}
</ul> </Text>
</Box>
<Chip
label={item.type === 'share' ? 'Акция' : 'Облигация'}
tone={item.type === 'share' ? 'info' : 'success'}
/>
</Box>
))}
</Surface>
</Box>
)} )}
</div> </Box>
); );
} }

View File

@ -0,0 +1,60 @@
# Pilot Migration — Implementation Plan
## Architecture
Миграция слоя представления без изменения логики. HomePage меняет JSX на DS-компоненты. SearchBar заменяет inline-стили на DS-компоненты + MUI `Box` (разрешён layout utility).
## Files
### Modify
- `apps/frontend/src/pages/home/ui/HomePage.tsx` — замена JSX
- `apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx` — замена JSX
### No changes
- `apps/frontend/src/widgets/search-bar/ui/SearchBar.test.tsx` — тесты не меняются (те же тексты)
- `apps/frontend/src/styles.css` — CSS-переменные всё ещё используются broker-страницами
## Step-by-step
### Step 1: Migrate HomePage
```tsx
// До
<div style={{ textAlign: 'center', paddingTop: 120 }}>
<h1 style={{ fontSize: 32, fontWeight: 700, marginBottom: 12 }}>MoexVibe</h1>
<p style={{ color: 'var(--color-text-secondary)', fontSize: 16, marginBottom: 32 }}>
Анализ акций и облигаций Московской биржи
</p>
<p style={{ color: '#888', fontSize: 13 }}>Введите название или тикер в строку поиска выше</p>
<p style={{ color: '#aaa', fontSize: 12, marginTop: 8 }}>
Данные задерживаются на 15 минут · Бесплатный API MOEX ISS
</p>
</div>
// После
import { Box } from '@mui/material';
import { Heading, Text } from '@moex-vibe/design-system';
<Box textAlign="center" pt={30}>
<Heading level={1} size="display">MoexVibe</Heading>
<Text tone="secondary">Анализ акций и облигаций Московской биржи</Text>
<Text tone="muted">Введите название или тикер в строку поиска выше</Text>
<Text tone="muted">Данные задерживаются на 15 минут · Бесплатный API MOEX ISS</Text>
</Box>
```
### Step 2: Migrate SearchBar
- `<input>``<TextField label="Поиск" hiddenLabel placeholder="Поиск акций и облигаций..." />`
- `<ul>` dropdown wrapper → `<Surface elevation="sm">` (с inline-стилями для absolute positioning)
- `<li>` loading → `<Text tone="muted">Загрузка...</Text>`
- `<li>` empty → `<Text tone="muted">Ничего не найдено</Text>`
- `<li>` result item → `<Box>` + `<Text>` + `<Chip>`
- Hover effect (`onMouseEnter/onMouseLeave`) → MUI `sx={{ '&:hover': { bgcolor: 'action.hover' } }}`
- Hardcoded hex colors → MUI theme tokens (`text.disabled`, `divider`, etc.)
### Step 3: Verify
- `npm run test:frontend` — все тесты проходят
- `npm run lint -w apps/frontend` — lint чист, нет неразрешённых MUI-импортов
- `npm run build:frontend` — сборка проходит

View File

@ -0,0 +1,31 @@
# Pilot Migration — Design System Adoption
## Status
Approved — утверждена пользователем 2026-06-21.
## Goal
Пилотная миграция существующих страниц на компоненты `@moex-vibe/design-system`. Подтвердить, что API дизайн-системы покрывает реальные сценарии, и зафиксировать gaps для следующей итерации.
## Scope
- `apps/frontend/src/pages/home/ui/HomePage.tsx` — миграция на `<Heading>`, `<Text>`, `<Box>`
- `apps/frontend/src/widgets/search-bar/ui/SearchBar.tsx` — миграция на `<TextField>`, `<Surface>`, `<Chip>`, `<Text>`, `<Box>`
## Out of scope
- Рефакторинг логики SearchBar (debounce, click outside, keyboard nav, navigate)
- Миграция других страниц/виджетов
- Добавление новых компонентов в DS
- Удаление всего `styles.css` (только orphaned CSS variables после миграции)
- Изменение поведения компонентов (все acceptance criteria остаются теми же)
## Acceptance Criteria
- [ ] HomePage не содержит inline-стилей; все стили — через DS-компоненты и MUI layout utilities
- [ ] SearchBar не содержит hardcoded hex-цветов; палитра — через DS tokens
- [ ] SearchBar states (loading, empty, results) используют соответствующие DS-компоненты
- [ ] Логика SearchBar не изменена — все тесты проходят
- [ ] ESLint `no-restricted-imports` не нарушен (только `Box` из MUI напрямую)
- [ ] `npm run test:frontend && npm run lint -w apps/frontend && npm run build:frontend` проходят

View File

@ -0,0 +1,27 @@
# Pilot Migration — Tasks
## 1. HomePage
- [x] Заменить h1 на `<Heading level={1} size="display">`
- [x] Заменить p с text-secondary на `<Text tone="secondary">`
- [x] Заменить p с #888/#aaa на `<Text tone="muted">`
- [x] Заменить div-wrapper на `<Box textAlign="center" pt={30}>`
- [x] Удалить все inline-стили
## 2. SearchBar
- [x] Заменить `<input>` на `<TextField label="Поиск" hiddenLabel>`
- [x] Заменить `<ul>` на `<Surface elevation="sm">` с positioning styles
- [x] Заменить loading text на `<Text tone="muted">`
- [x] Заменить empty text на `<Text tone="muted">`
- [x] Заменить `<li>` results на `<Box>` с flex layout
- [x] Заменить hardcoded hex на theme tokens
- [x] Заменить JS hover handlers на MUI `sx={{ '&:hover': { bgcolor: 'action.hover' } }}`
- [x] Заменить тип "Акция"/"Облигация" на `<Chip>`
## 3. Verification
- [x] `npm run test:frontend` — 23 suites, 111 passed
- [x] `npm run lint -w apps/frontend` — чист, 0 warnings
- [x] `npm run build:frontend` — проходит
- [x] ESLint `no-restricted-imports` не нарушен (использован `@mui/material/Box` deep import)