Версия: 1.0
Дата: 2025-11-08
Статус: В разработке
Наш проект - это современный SaaS-сервис для управления заказами с маркетплейсов.
Вдохновение: Airtable, Notion, Linear, Retool
Принципы:
- Компактность - много данных на экране без скроллинга
- Консистентность - единый стиль везде
- Скорость - быстрая навигация, минимум кликов
- Ясность - понятные иконки, статусы, действия
┌─────────────────────────────────────────────────────┐
│ Header: Logo + Title │
├──────────┬──────────────────────────────────────────┤
│ │ │
│ Sidebar │ Main Content Area │
│ (Menu) │ ┌────────────────────────────────────┐ │
│ │ │ Page Header (Title + Actions) │ │
│ │ ├────────────────────────────────────┤ │
│ │ │ Filters / Tabs │ │
│ │ ├────────────────────────────────────┤ │
│ │ │ Data Table / Content │ │
│ │ │ │ │
│ │ └────────────────────────────────────┘ │
└──────────┴──────────────────────────────────────────┘
Правила:
- Sidebar всегда слева (Streamlit стандарт)
- Контент всегда на всю ширину (layout="wide")
- Никаких вложенных sidebar
Структура:
🏠 Главная
📡 Каналы
📦 Товары
📋 Заказы
📊 Аналитика
⚙️ Настройки
🧪 Тесты (только admin)
Правила:
- Иконка + название (emoji для быстрого распознавания)
- Активная страница подсвечивается автоматически
- Максимум 7-8 пунктов
Формат:
Каналы > Управление: Ozon Магазин #1
Когда использовать:
- При переходе на детальную страницу
- При многоуровневой навигации
Реализация: Показывать в верхней части страницы, кликабельные
Список:
- Таблица со всеми записями
- Кнопка "Управление" / "Открыть" в каждой строке
Деталь:
- Кнопка "← Назад к списку" в верхней части
- При клике возвращаемся к списку
- При входе через меню всегда показываем список
Код-паттерн:
# В session_state хранится ID выбранного объекта
if 'selected_channel_id' in st.session_state:
show_detail_page(st.session_state.selected_channel_id)
else:
show_list_page()
ВАЖНО: При клике на пункт меню всегда очищаем session_state!
Стиль: Компактный, как в Airtable/Notion
Характеристики:
- Высота строки: 36-40px (компактно!)
- Заголовки колонок: жирным, uppercase, серый фон
- Чередующиеся строки: белый / #F9FAFB
- Hover эффект: #F3F4F6
- Границы: тонкие 1px #E5E7EB
HTML/CSS шаблон:
<style>
.compact-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.compact-table thead th {
background: #F3F4F6;
color: #374151;
font-weight: 600;
text-transform: uppercase;
font-size: 12px;
padding: 8px 12px;
text-align: left;
border-bottom: 2px solid #E5E7EB;
cursor: pointer;
user-select: none;
}
.compact-table thead th:hover {
background: #E5E7EB;
}
.compact-table tbody tr {
border-bottom: 1px solid #E5E7EB;
}
.compact-table tbody tr:nth-child(even) {
background: #F9FAFB;
}
.compact-table tbody tr:hover {
background: #F3F4F6;
}
.compact-table tbody td {
padding: 10px 12px;
vertical-align: middle;
}
</style>
Формат: Название ↑ или Название ↓
Реализация:
def sortable_header(column_name, current_sort_column, sort_direction):
arrow = ""
if current_sort_column == column_name:
arrow = " ↑" if sort_direction == "asc" else " ↓"
return f"{column_name}{arrow}"
Клик: Переключает направление сортировки
Стандартные колонки для списков:
Пример:
┌───┬──────────────────┬────────┬────────┬────────────┬──────────┐
│ 🟢│ Ozon Магазин #1 │ OZON │ ООО 1 │ 2025-11-08 │ [Открыть]│
│ 🔴│ WB Магазин #2 │ WB │ ООО 2 │ 2025-11-07 │ [Открыть]│
└───┴──────────────────┴────────┴────────┴────────────┴──────────┘
1. Встроенная форма (в той же странице)
- Для простых действий
- Пример: Фильтры, быстрое редактирование
2. Выдвижная панель (Drawer)
- Для средних форм
- Появляется справа, перекрывает часть экрана
- Пример: Создание/редактирование записи
3. Модальное окно (Modal)
- Для критичных действий
- Затемняет фон
- Пример: Подтверждение удаления
4. Отдельная страница (Full Page)
- Для сложных многошаговых форм
- Пример: Визард подключения канала
Расположение: Внизу справа
Порядок:
[Отмена] [Сохранить]
Цвета:
- Основная кнопка: синий #3B82F6
- Отмена: серый #6B7280
- Удаление: красный #EF4444
Размеры:
- Small: 28px высота, 12px padding
- Medium: 36px высота, 16px padding
- Large: 44px высота, 20px padding
Типы:
# Primary (синяя)
st.button("Сохранить", type="primary")
# Secondary (серая)
st.button("Отмена")
# Icon (только иконка)
st.button("🗑️", key="delete")
Иконки:
- 🟢 Активен / Успешно
- 🔴 Неактивен / Ошибка
- 🟡 Ожидание / В процессе
- ⚪ Новый / Неизвестно
Бейджи:
<span style="background: #10B981; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px;">
Активен
</span>
Для группировки информации:
<div style="
background: white;
border: 1px solid #E5E7EB;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
">
<h3>Заголовок</h3>
<p>Контент</p>
</div>
Основные цвета:
Primary (синий): #3B82F6
Success (зелёный): #10B981
Warning (жёлтый): #F59E0B
Error (красный): #EF4444
Нейтральные (серые):
Gray 50: #F9FAFB (фон строк)
Gray 100: #F3F4F6 (фон заголовков)
Gray 200: #E5E7EB (границы)
Gray 400: #9CA3AF (вторичный текст)
Gray 700: #374151 (основной текст)
Gray 900: #111827 (заголовки)
Фоны:
Page Background: #FFFFFF
Sidebar: #F9FAFB
Card: #FFFFFF
Шрифты:
- Основной: system-ui, -apple-system, sans-serif
- Моноширинный: 'SF Mono', Monaco, monospace
Размеры:
H1 (Page Title): 24px, bold
H2 (Section): 20px, semibold
H3 (Card Title): 16px, semibold
Body: 14px, regular
Small: 12px, regular
Button: 14px, medium
Line Height:
- Заголовки: 1.2
- Текст: 1.5
Система 4px grid:
xs: 4px
sm: 8px
md: 16px
lg: 24px
xl: 32px
2xl: 48px
Правила:
- Между секциями: 24px
- Между элементами в форме: 16px
- Padding карточек: 16px
- Margin кнопок: 8px
Источник: Unicode Emoji (встроенные)
Стандартные иконки:
Действия:
✏️ Редактировать
🗑️ Удалить
👁️ Просмотр
📥 Загрузить
📤 Экспорт
🔄 Обновить
➕ Добавить
✅ Подтвердить
❌ Отменить
Статусы:
🟢 Активно
🔴 Неактивно
🟡 Ожидание
⚪ Новое
Объекты:
📡 Канал
📦 Товар
📋 Заказ
🏢 Юр.лицо
🏪 Склад
🚚 Доставка
Spinner:
with st.spinner("Загрузка..."):
# код
Skeleton (для таблиц):
┌───┬──────────────────┬────────┐
│ ▓ │ ▓▓▓▓▓▓▓▓▓ │ ▓▓▓▓ │
│ ▓ │ ▓▓▓▓▓▓▓ │ ▓▓▓ │
└───┴──────────────────┴────────┘
Формат:
┌─────────────────────────────────┐
│ 📋 │
│ Нет данных │
│ Добавьте первую запись │
│ [+ Добавить] │
└─────────────────────────────────┘
Формат:
st.error("❌ Ошибка: текст ошибки")
Брейкпоинты:
Mobile: < 768px (не поддерживается)
Tablet: 768-1024px (минимально)
Desktop: > 1024px (основной)
Правила:
- Приложение оптимизировано для Desktop
- Минимальная ширина: 1024px
- Таблицы: горизонтальный скролл если нужно
Кнопки: Затемнение на 10%
Строки таблицы: Фон #F3F4F6
Ссылки: Подчёркивание
Стандартная анимация:
transition: all 0.15s ease-in-out;
Используется для:
- Hover состояний
- Раскрытия/скрытия элементов
Всегда в начале файла:
st.set_page_config(
page_title="Marketplace MVP",
page_icon="🛒",
layout="wide",
initial_sidebar_state="expanded"
)
Создать файл: /home/claude-helper/marketplace-mvp/styles/main.css
Подключить:
def load_css():
with open('styles/main.css') as f:
st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
"""
Название страницы - краткое описание
"""
import streamlit as st
from core.auth import check_authentication
# Проверка авторизации
if not check_authentication():
st.warning("⚠️ Требуется авторизация")
st.stop()
# Загрузка стилей
load_css()
# Заголовок
st.title("📡 Название страницы")
# Логика списка vs деталей
if 'selected_id' in st.session_state:
show_detail_page()
else:
show_list_page()
def show_sortable_table(data, columns):
# Сортировка
if 'sort_column' not in st.session_state:
st.session_state.sort_column = columns[0]
st.session_state.sort_direction = 'asc'
# Заголовки
cols = st.columns(len(columns))
for i, col_name in enumerate(columns):
with cols[i]:
if st.button(
sortable_header(col_name,
st.session_state.sort_column,
st.session_state.sort_direction),
key=f"sort_{col_name}"
):
if st.session_state.sort_column == col_name:
st.session_state.sort_direction = 'desc' if st.session_state.sort_direction == 'asc' else 'asc'
else:
st.session_state.sort_column = col_name
st.session_state.sort_direction = 'asc'
st.rerun()
# Данные
sorted_data = sorted(data,
key=lambda x: x[st.session_state.sort_column],
reverse=(st.session_state.sort_direction == 'desc'))
for row in sorted_data:
cols = st.columns(len(columns))
for i, col_name in enumerate(columns):
with cols[i]:
st.write(row[col_name])
/styles/main.css с основными стилями/components/table.py - компонент таблицы/components/buttons.py - стандартные кнопки/utils/navigation.py - хелперы навигацииСтруктура проекта:
/home/claude-helper/marketplace-mvp/
├── app.py # Главная точка входа
├── config.yaml # Конфигурация
├── requirements.txt # Зависимости
├── STYLE-GUIDE.md # Этот файл
├── PROJECT-MASTER.txt # Мастер-файл проекта
├── CLAUDE.md # Документация для Claude
├── core/ # Ядро приложения
│ ├── __init__.py
│ ├── auth.py # Авторизация
│ ├── config.py # Настройки
│ └── database.py # БД
├── database/ # Модели данных
│ ├── __init__.py
│ └── models.py
├── modules/ # Бизнес-логика
│ ├── api/ # API интеграции
│ │ ├── ozon.py
│ │ └── wildberries.py
│ ├── legal_entities.py # Работа с юр.лицами
│ └── test_data.py # Тестовые данные
├── pages/ # Страницы Streamlit
│ ├── 01_🏠_Главная.py
│ ├── 02_📡_Каналы.py
│ ├── 03_📦_Товары.py
│ ├── 04_📋_Заказы.py
│ ├── 05_📊_Аналитика.py
│ ├── 06_⚙️_Настройки.py
│ └── 07_🧪_Тесты.py
├── components/ # UI компоненты (будущее)
│ ├── table.py
│ ├── buttons.py
│ └── forms.py
├── styles/ # CSS стили
│ └── main.css
├── utils/ # Утилиты
│ ├── navigation.py
│ └── formatters.py
├── data/ # Данные
│ └── marketplace.db
└── logs/ # Логи
└── app.log
Правила именования:
Python файлы:
- Модули: snake_case.py
- Классы: PascalCase
- Функции: snake_case()
- Константы: UPPER_SNAKE_CASE
- Приватные: _leading_underscore
Страницы Streamlit:
NN_ICON_Название.py
NN - номер для сортировки (01, 02, 03...)
ICON - эмодзи иконка
Название - кириллица или латиница
Пример: 02_📡_Каналы.py
Python:
# ✅ Хорошо
user_id = 1
channel_name = "Ozon Store"
get_current_user()
calculate_total_price()
# ❌ Плохо
userId = 1 # не camelCase
nm = "Ozon" # неясное имя
getUserData() # не snake_case
SQL/Database:
# Таблицы: множественное число, snake_case
users
legal_entities
delivery_services
# Колонки: snake_case
user_id
created_at
name_full
Session State:
# Формат: page_feature_data
st.session_state.channel_wizard_step
st.session_state.selected_channel_id
st.session_state.sort_column
st.session_state.filter_status
IDs и Keys:
# Формат: action_object_identifier
key="edit_channel_123"
key="delete_order_456"
key="sort_name"
key="filter_status"
Функции:
def get_company_by_inn(inn: str) -> dict:
"""
Получить данные компании по ИНН из API.
Args:
inn: ИНН компании (10 или 12 цифр)
Returns:
dict: Данные компании или None если не найдено
Raises:
ValueError: Если ИНН невалидный
"""
pass
Классы:
class OzonAPI:
"""
Клиент для работы с Ozon Seller API.
Attributes:
client_id: ID клиента Ozon
api_key: API ключ
Methods:
get_seller_info(): Получить информацию о продавце
get_warehouses(): Получить список складов
"""
pass
Когда писать:
- Сложная бизнес-логика
- Workarounds и хаки
- TODO/FIXME/HACK
# TODO: Добавить кеширование
# FIXME: Не работает для WB API
# HACK: Временное решение до версии 2.0
# Проверяем ИНН по контрольной сумме
if len(inn) == 10:
# Для юр.лиц
checksum = calculate_inn_checksum(inn[:9])
else:
# Для ИП
checksum = calculate_inn_checksum(inn[:11])
Разделители:
# ============================================
# КОНСТАНТЫ И НАСТРОЙКИ
# ============================================
APP_NAME = "Marketplace MVP"
VERSION = "1.0"
# ============================================
# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
# ============================================
def helper():
pass
# ============================================
# ОСНОВНОЙ КОД
# ============================================
3 уровня:
1. Validation - на входе
2. Try/Except - в коде
3. User Message - на выходе
API вызовы:
try:
api = OzonAPI(client_id, api_key)
seller_info = api.get_seller_info()
st.success("✅ Подключение успешно")
except OzonAuthError as e:
st.error(f"❌ Ошибка авторизации: {e}")
except OzonAPIError as e:
st.error(f"❌ Ошибка API: {e}")
except Exception as e:
st.error(f"❌ Неизвестная ошибка: {e}")
if APP_MODE == "test":
st.exception(e)
Database операции:
session = get_session()
try:
channel = Channel(...)
session.add(channel)
session.commit()
st.success("✅ Канал создан")
except IntegrityError:
session.rollback()
st.error("❌ Канал с таким Client ID уже существует")
except Exception as e:
session.rollback()
st.error(f"❌ Ошибка БД: {e}")
finally:
session.close()
Формат:
[ИКОНКА] [ТИП]: [ОПИСАНИЕ]
✅ Успех: Канал успешно создан
❌ Ошибка: Не удалось подключиться к API
⚠️ Внимание: Данные могут быть устаревшими
ℹ️ Инфо: Для работы нужно настроить каналы
Не писать:
- Технические детали (stack trace)
- "Something went wrong"
- Коды ошибок без описания
Писать:
- Что произошло
- Почему
- Как исправить
Streamlit cache:
@st.cache_data(ttl=3600) # 1 час
def get_channels_list():
"""Закешированный список каналов"""
session = get_session()
try:
return session.query(Channel).all()
finally:
session.close()
@st.cache_resource
def get_database_connection():
"""Singleton подключение к БД"""
return create_engine(DATABASE_URL)
Когда кешировать:
- Справочные данные (редко меняются)
- Тяжелые вычисления
- API запросы (с осторожностью)
Когда НЕ кешировать:
- Пользовательский ввод
- Операции записи
- Данные в реальном времени
Для больших списков (>100 записей):
PAGE_SIZE = 50
if 'page' not in st.session_state:
st.session_state.page = 1
total_records = session.query(Order).count()
total_pages = (total_records + PAGE_SIZE - 1) // PAGE_SIZE
offset = (st.session_state.page - 1) * PAGE_SIZE
orders = session.query(Order).limit(PAGE_SIZE).offset(offset).all()
# Пагинатор
col1, col2, col3 = st.columns([1, 2, 1])
with col1:
if st.button("← Назад", disabled=(st.session_state.page == 1)):
st.session_state.page -= 1
st.rerun()
with col2:
st.write(f"Страница {st.session_state.page} из {total_pages}")
with col3:
if st.button("Вперёд →", disabled=(st.session_state.page == total_pages)):
st.session_state.page += 1
st.rerun()
Подгружать данные по требованию:
# Сначала показываем основной список
for channel in channels:
with st.expander(channel.name):
if st.button("Показать детали", key=f"details_{channel.id}"):
# Только сейчас грузим детали
warehouses = load_warehouses(channel.id)
st.write(warehouses)
Проверка на каждой странице:
from core.auth import check_authentication
if not check_authentication():
st.warning("⚠️ Требуется авторизация")
st.stop()
НЕ коммитить:
- API ключи
- Пароли
- Токены
- Секретные ключи
Использовать:
# .gitignore
config.yaml
.env
*.db
Хранить в:
- config.yaml (НЕ в git)
- Environment variables
- Encrypted database fields
Всегда проверять:
def create_channel(client_id: str, api_key: str):
# Валидация
if not client_id or len(client_id) < 6:
raise ValueError("Client ID должен быть минимум 6 символов")
if not api_key or len(api_key) < 20:
raise ValueError("API ключ слишком короткий")
# Sanitization
client_id = client_id.strip()
# Создание
channel = Channel(client_id=client_id, api_key=api_key)
1. Playwright E2E:
// Тестируем UI flow
await page.fill('input[type="text"]', 'admin');
await page.click('button:has-text("Login")');
await expect(page.locator('text=Welcome')).toBeVisible();
2. Python Unit:
def test_inn_validation():
assert validate_inn("1234567890") == True
assert validate_inn("123") == False
3. Ручное тестирование:
- Чеклист основных сценариев
- Тестирование на разных браузерах
Минимум:
- Критичные бизнес-процессы (авторизация, создание заказа)
- API интеграции
- Расчеты и валидация
Не обязательно:
- UI компоненты (Streamlit сам протестирован)
- Simple CRUD
import logging
logging.debug("Детальная инфа для разработки")
logging.info("Обычные события (старт/стоп)")
logging.warning("Что-то странное но не критично")
logging.error("Ошибка, нужно разобраться")
logging.critical("Всё сломалось!")
# core/config.py
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
handlers=[
logging.FileHandler('logs/app.log'),
logging.StreamHandler()
]
)
Пример:
2025-11-08 12:34:56 [INFO] modules.api.ozon: Подключение к Ozon API (client_id=123456)
2025-11-08 12:34:57 [ERROR] modules.api.ozon: Ошибка авторизации: Invalid API key
ДА:
- API запросы и ответы
- Создание/изменение записей в БД
- Ошибки и исключения
- Важные бизнес-события
НЕТ:
- Пароли и API ключи
- Персональные данные
- Каждый запрос к БД (в production)
Формат: MAJOR.MINOR.PATCH
1.0.0 - Первая стабильная версия
1.1.0 - Добавлена новая фича
1.1.1 - Исправлен баг
2.0.0 - Breaking changes
Где указывать:
# core/config.py
VERSION = "1.0.0"
# app.py
st.sidebar.write(f"v{VERSION}")
Файл: CHANGELOG.md
# Changelog
## [1.1.0] - 2025-11-08
### Added
- Компактные таблицы с сортировкой
- Страница управления каналом
### Changed
- Улучшена навигация
### Fixed
- Исправлена ошибка авторизации
Минимум WCAG AA:
- Текст: контраст 4.5:1
- Заголовки: контраст 3:1
Проверить:
Текст #374151 на фоне #FFFFFF = 12.63:1 ✅
Кнопка #3B82F6 на фоне #FFFFFF = 4.56:1 ✅
Должно работать:
- Tab - переход между элементами
- Enter - активация кнопок/ссылок
- Escape - закрытие модалов
Streamlit:
# Автоматически работает для:
st.button()
st.text_input()
st.selectbox()
Альт-тексты:
# НЕ ТОЛЬКО иконки
st.button("🗑️") # ❌
# Добавить текст
st.button("🗑️ Удалить") # ✅
Сейчас: Только русский язык
Будущее: Мультиязычность
Структура:
# locales/ru.py
MESSAGES = {
"login.title": "Вход в систему",
"login.username": "Имя пользователя",
"login.password": "Пароль",
"button.save": "Сохранить",
"button.cancel": "Отмена",
}
# locales/en.py
MESSAGES = {
"login.title": "Sign In",
"login.username": "Username",
"login.password": "Password",
"button.save": "Save",
"button.cancel": "Cancel",
}
Использование:
from locales import get_message
st.title(get_message("login.title"))
Сейчас:
from datetime import datetime
# Формат даты: DD.MM.YYYY
date_str = datetime.now().strftime("%d.%m.%Y")
# Числа: 1 234,56
amount = f"{price:,.2f}".replace(",", " ").replace(".", ",")
Зафиксировать версии:
streamlit==1.28.0
streamlit-authenticator==0.3.3
sqlalchemy==2.0.23
bcrypt==4.1.2
Обновить:
pip freeze > requirements.txt
Development:
APP_MODE = "test"
DEBUG = True
DATABASE_URL = "sqlite:///data/marketplace.db"
Production:
APP_MODE = "production"
DEBUG = False
DATABASE_URL = "postgresql://..."
# /healthcheck endpoint (если есть)
@app.get("/health")
def health():
return {
"status": "ok",
"version": VERSION,
"database": check_db_connection()
}
Отслеживать:
- Uptime
- Response time
- Error rate
- Active users
- Database size
Настроить уведомления:
- App crashed
- Database full
- API rate limit exceeded
Рекомендуется создать:
Этот документ - living document. Обновляется по мере развития проекта.
Последнее обновление: 2025-11-08
Версия: 1.1