diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx index 64654c5..65634bb 100644 --- a/client/src/app/App.tsx +++ b/client/src/app/App.tsx @@ -4,6 +4,7 @@ import { AppRoutes } from '@/app/routes' import { NotificationStack } from '@/shared/ui/NotificationStack' import { ErrorBoundary } from '@/shared/ui/ErrorBoundary' import { NoiseOverlay } from '@/shared/ui/NoiseOverlay' +import { DemoOverlay } from '@/shared/ui/DemoOverlay' export function App() { return ( @@ -14,6 +15,7 @@ export function App() { + ) diff --git a/client/src/shared/ui/DemoOverlay.tsx b/client/src/shared/ui/DemoOverlay.tsx new file mode 100644 index 0000000..3916dff --- /dev/null +++ b/client/src/shared/ui/DemoOverlay.tsx @@ -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 ( + <> + + + + + ) +} diff --git a/client/src/shared/ui/__tests__/DemoOverlay.test.tsx b/client/src/shared/ui/__tests__/DemoOverlay.test.tsx new file mode 100644 index 0000000..3130c6b --- /dev/null +++ b/client/src/shared/ui/__tests__/DemoOverlay.test.tsx @@ -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('@/shared/config') + return { + ...actual, + get IS_DEMO_MODE() { + return mockDemoMode + }, + } +}) + +import { DemoOverlay } from '../DemoOverlay' + +describe('DemoOverlay', () => { + it('рендерит водяной знак и плашку когда демо включён', () => { + mockDemoMode = true + const { container } = render() + + 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() + expect(container.textContent).toBe('') + }) +}) diff --git a/docs/superpowers/specs/2026-06-03-demo-overlay-design.md b/docs/superpowers/specs/2026-06-03-demo-overlay-design.md new file mode 100644 index 0000000..2d2c725 --- /dev/null +++ b/docs/superpowers/specs/2026-06-03-demo-overlay-design.md @@ -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` на одном уровне с ``, вне роутов: + +```tsx + + +``` + +`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` | diff --git a/server/prisma/prisma/dev.db b/server/prisma/prisma/dev.db index abbf38c..7065504 100644 Binary files a/server/prisma/prisma/dev.db and b/server/prisma/prisma/dev.db differ