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() {
return (
<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 минут &middot; Бесплатный API MOEX ISS
</p>
</div>
<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>
);
}

View File

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