add diaposine
This commit is contained in:
@@ -4,6 +4,7 @@ import { AppRoutes } from '@/app/routes'
|
|||||||
import { NotificationStack } from '@/shared/ui/NotificationStack'
|
import { NotificationStack } from '@/shared/ui/NotificationStack'
|
||||||
import { ErrorBoundary } from '@/shared/ui/ErrorBoundary'
|
import { ErrorBoundary } from '@/shared/ui/ErrorBoundary'
|
||||||
import { NoiseOverlay } from '@/shared/ui/NoiseOverlay'
|
import { NoiseOverlay } from '@/shared/ui/NoiseOverlay'
|
||||||
|
import { DemoOverlay } from '@/shared/ui/DemoOverlay'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
@@ -14,6 +15,7 @@ export function App() {
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<NotificationStack />
|
<NotificationStack />
|
||||||
<NoiseOverlay />
|
<NoiseOverlay />
|
||||||
|
<DemoOverlay />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AppProviders>
|
</AppProviders>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
import { IS_DEMO_MODE } from '@/shared/config'
|
||||||
|
|
||||||
|
export function DemoOverlay() {
|
||||||
|
const theme = useTheme()
|
||||||
|
const isDark = theme.palette.mode === 'dark'
|
||||||
|
|
||||||
|
if (!IS_DEMO_MODE) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
aria-hidden="true"
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: 9990,
|
||||||
|
fontSize: '10vw',
|
||||||
|
fontWeight: 900,
|
||||||
|
color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.04)',
|
||||||
|
transform: 'rotate(-30deg)',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ДЕМО
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
aria-hidden="true"
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: 16,
|
||||||
|
right: 16,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: 9991,
|
||||||
|
px: 2,
|
||||||
|
py: 0.75,
|
||||||
|
borderRadius: 1,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: '0.04em',
|
||||||
|
color: isDark ? 'rgba(255,255,255,0.6)' : '#fff',
|
||||||
|
bgcolor: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.6)',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ДЕМО-РЕЖИМ
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
let mockDemoMode = true
|
||||||
|
|
||||||
|
vi.mock('@/shared/config', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('@/shared/config')>('@/shared/config')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
get IS_DEMO_MODE() {
|
||||||
|
return mockDemoMode
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
import { DemoOverlay } from '../DemoOverlay'
|
||||||
|
|
||||||
|
describe('DemoOverlay', () => {
|
||||||
|
it('рендерит водяной знак и плашку когда демо включён', () => {
|
||||||
|
mockDemoMode = true
|
||||||
|
const { container } = render(<DemoOverlay />)
|
||||||
|
|
||||||
|
const text = container.textContent
|
||||||
|
expect(text).toContain('ДЕМО')
|
||||||
|
expect(text).toContain('ДЕМО-РЕЖИМ')
|
||||||
|
|
||||||
|
const allBoxes = container.querySelectorAll('.MuiBox-root')
|
||||||
|
expect(allBoxes.length).toBeGreaterThanOrEqual(2)
|
||||||
|
|
||||||
|
const [watermark, badge] = allBoxes
|
||||||
|
|
||||||
|
expect(watermark.getAttribute('aria-hidden')).toBe('true')
|
||||||
|
expect(watermark.textContent).toBe('ДЕМО')
|
||||||
|
|
||||||
|
expect(badge.getAttribute('aria-hidden')).toBe('true')
|
||||||
|
expect(badge.textContent).toBe('ДЕМО-РЕЖИМ')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('не рендерит ничего когда демо выключен', () => {
|
||||||
|
mockDemoMode = false
|
||||||
|
const { container } = render(<DemoOverlay />)
|
||||||
|
expect(container.textContent).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# DemoOverlay — индикация демо-режима
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Демо-режим активируется через `VITE_DEMO_MODE=true` (`client/.env.local`). Сейчас есть только `DemoBanner` (Alert в потоке страницы, не фиксированный). Нужно добавить постоянную визуальную индикацию — оверлей, который не мешает взаимодействию с сайтом.
|
||||||
|
|
||||||
|
## Что делаем
|
||||||
|
|
||||||
|
Новый компонент `DemoOverlay` в `client/src/shared/ui/DemoOverlay.tsx`.
|
||||||
|
|
||||||
|
Два фиксированных слоя, оба `pointer-events: none`:
|
||||||
|
|
||||||
|
1. **Водяной знак** — крупная надпись «ДЕМО», полупрозрачная, повёрнута на ~-30°, по центру экрана.
|
||||||
|
2. **Плашка** — правый нижний угол, скруглённая полупрозрачная тёмная плашка с текстом «ДЕМО-РЕЖИМ».
|
||||||
|
|
||||||
|
Оба рендерятся только при `IS_DEMO_MODE === true`.
|
||||||
|
|
||||||
|
## Размещение
|
||||||
|
|
||||||
|
В `App.tsx` на одном уровне с `<NoiseOverlay />`, вне роутов:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<NoiseOverlay />
|
||||||
|
<DemoOverlay />
|
||||||
|
```
|
||||||
|
|
||||||
|
`DemoBanner` (существующий Alert в MainLayout) — не трогаем, остаётся как есть.
|
||||||
|
|
||||||
|
## Водяной знак
|
||||||
|
|
||||||
|
- Текст: `ДЕМО`
|
||||||
|
- Размер шрифта: `10vw` (адаптивный)
|
||||||
|
- Поворот: `rotate(-30deg)`
|
||||||
|
- Цвет: `rgba(0,0,0,0.04)` (тёмная тема: `rgba(255,255,255,0.04)`)
|
||||||
|
- Позиция: `position: fixed`, `inset: 0`, центрирование через flex
|
||||||
|
- z-index: `9990`
|
||||||
|
|
||||||
|
## Плашка
|
||||||
|
|
||||||
|
- Текст: `ДЕМО-РЕЖИМ`
|
||||||
|
- Позиция: `position: fixed`, `bottom: 16px`, `right: 16px`
|
||||||
|
- Фон: `rgba(0,0,0,0.6)` (тёмная тема: `rgba(255,255,255,0.08)`)
|
||||||
|
- Цвет текста: `#fff` (тёмная тема: `rgba(255,255,255,0.6)`)
|
||||||
|
- Паддинги: `6px 16px`
|
||||||
|
- Скругление: `8px`
|
||||||
|
- Размер шрифта: `12px`, `font-weight: 600`
|
||||||
|
- z-index: `9991`
|
||||||
|
|
||||||
|
## Тёмная тема
|
||||||
|
|
||||||
|
Компонент читает тему через `useTheme()` из MUI и применяет соответствующие цвета для watermark и плашки.
|
||||||
|
|
||||||
|
## Тесты
|
||||||
|
|
||||||
|
Проверяем:
|
||||||
|
- Компонент рендерится когда `IS_DEMO_MODE === true` (водяной знак + плашка видны)
|
||||||
|
- Компонент не рендерится когда `IS_DEMO_MODE === false`
|
||||||
|
- Плашка в правом нижнем углу (проверяем CSS-свойства)
|
||||||
|
- `pointer-events: none` на обоих элементах
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
| Действие | Файл |
|
||||||
|
|----------|------|
|
||||||
|
| Создать | `client/src/shared/ui/DemoOverlay.tsx` |
|
||||||
|
| Изменить | `client/src/app/App.tsx` |
|
||||||
|
| Создать | `client/src/shared/ui/__tests__/DemoOverlay.test.tsx` |
|
||||||
Binary file not shown.
Reference in New Issue
Block a user