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:
parent
3534ec4f77
commit
bd33412691
@ -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 минут · Бесплатный 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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
60
docs/features/pilot-migration/plan.md
Normal file
60
docs/features/pilot-migration/plan.md
Normal 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` — сборка проходит
|
||||
31
docs/features/pilot-migration/spec.md
Normal file
31
docs/features/pilot-migration/spec.md
Normal 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` проходят
|
||||
27
docs/features/pilot-migration/tasks.md
Normal file
27
docs/features/pilot-migration/tasks.md
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user