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