Files
news_all_spo/web/designer.py
2026-05-15 03:31:28 +05:00

254 lines
9.9 KiB
Python

import flet as ft
# ─── Шрифты ──────────────────────────────────────────────────────────────────
FONT_UI = "Inter"
FONT_SERIF = "Merriweather"
FONT_DISPLAY = "Playfair Display"
FONT_MONO = "JetBrains Mono"
ARTICLE_FONTS: dict[str, str] = {
"Merriweather": "Merriweather — для чтения (засечки)",
"Inter": "Inter — современный (без засечек)",
"Playfair Display": "Playfair Display — газетный",
"Roboto": "Roboto — нейтральный",
"JetBrains Mono": "JetBrains Mono — моноширинный",
}
GOOGLE_FONTS_JS = """
(function(){
if(document.getElementById('gfonts-news')) return;
var l=document.createElement('link');
l.id='gfonts-news';
l.rel='stylesheet';
l.href='https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=Merriweather:ital,wght@0,300;0,400;0,700;1,400&family=JetBrains+Mono:wght@400;500;600&family=Roboto:wght@400;500;700&display=swap';
document.head.appendChild(l);
})();
"""
# ─── Цвета ───────────────────────────────────────────────────────────────────
PRIMARY = "#2787F5" # VK синий
PRIMARY_LIGHT = "#5BA4F7"
PRIMARY_DARK = "#1565C0"
ACCENT = "#FF3F4B" # красный акцент (лайки / важное)
SUCCESS = "#3DC47E"
WARNING = "#FFA726"
ERROR = "#E64646"
SURFACE = "#FFFFFF"
BACKGROUND = "#F0F2F5" # VK светло-серый фон
CARD_BG = "#FFFFFF"
TEXT_PRIMARY = "#050505"
TEXT_SECONDARY = "#818C99" # VK серый для мета-текста
DIVIDER = "#E5E9EF"
ADMIN_SIDEBAR = "#1A237E"
ADMIN_SIDEBAR_TEXT = "#E8EAF6"
ADMIN_SIDEBAR_ACTIVE = "#283593"
# ─── Типографика ─────────────────────────────────────────────────────────────
def headline(text: str, size: int = 28, color: str = TEXT_PRIMARY, weight=ft.FontWeight.BOLD, font: str = FONT_UI) -> ft.Text:
return ft.Text(text, size=size, color=color, weight=weight, font_family=font)
def body(text: str, size: int = 14, color: str = TEXT_PRIMARY, font: str = FONT_UI) -> ft.Text:
return ft.Text(text, size=size, color=color, font_family=font)
def caption(text: str, color: str = TEXT_SECONDARY) -> ft.Text:
return ft.Text(text, size=12, color=color, font_family=FONT_UI)
# ─── Кнопки ──────────────────────────────────────────────────────────────────
def btn_primary(text: str, on_click=None, icon=None, width: int | None = None) -> ft.ElevatedButton:
return ft.ElevatedButton(
text=text,
icon=icon,
on_click=on_click,
width=width,
style=ft.ButtonStyle(
bgcolor=PRIMARY,
color=ft.colors.WHITE,
shape=ft.RoundedRectangleBorder(radius=8),
padding=ft.padding.symmetric(horizontal=20, vertical=12),
),
)
def btn_outlined(text: str, on_click=None, icon=None, color: str = PRIMARY) -> ft.OutlinedButton:
return ft.OutlinedButton(
text=text,
icon=icon,
on_click=on_click,
style=ft.ButtonStyle(
color=color,
side=ft.BorderSide(1, color),
shape=ft.RoundedRectangleBorder(radius=8),
padding=ft.padding.symmetric(horizontal=16, vertical=10),
),
)
def btn_danger(text: str, on_click=None, icon=None) -> ft.ElevatedButton:
return ft.ElevatedButton(
text=text,
icon=icon,
on_click=on_click,
style=ft.ButtonStyle(
bgcolor=ERROR,
color=ft.colors.WHITE,
shape=ft.RoundedRectangleBorder(radius=8),
),
)
def btn_icon(icon, tooltip: str = "", on_click=None, color: str = TEXT_SECONDARY) -> ft.IconButton:
return ft.IconButton(icon=icon, tooltip=tooltip, on_click=on_click, icon_color=color, icon_size=20)
# ─── Поля ввода ──────────────────────────────────────────────────────────────
def text_field(
label: str,
value: str = "",
on_change=None,
multiline: bool = False,
min_lines: int = 1,
max_lines: int | None = 1,
hint: str = "",
password: bool = False,
expand: bool = False,
read_only: bool = False,
prefix_icon=None,
) -> ft.TextField:
return ft.TextField(
label=label,
value=value,
on_change=on_change,
multiline=multiline,
min_lines=min_lines,
max_lines=max_lines,
hint_text=hint,
password=password,
can_reveal_password=password,
expand=expand,
read_only=read_only,
prefix_icon=prefix_icon,
border_color=DIVIDER,
focused_border_color=PRIMARY,
label_style=ft.TextStyle(color=TEXT_SECONDARY, size=13, font_family=FONT_UI),
text_style=ft.TextStyle(color=TEXT_PRIMARY, size=14, font_family=FONT_UI),
border_radius=8,
content_padding=ft.padding.symmetric(horizontal=12, vertical=10),
)
def dropdown(label: str, options: list[ft.dropdown.Option], value: str | None = None, on_change=None) -> ft.Dropdown:
return ft.Dropdown(
label=label,
value=value,
options=options,
on_change=on_change,
border_color=DIVIDER,
focused_border_color=PRIMARY,
label_style=ft.TextStyle(color=TEXT_SECONDARY, size=13),
border_radius=8,
)
# ─── Карточки ────────────────────────────────────────────────────────────────
def card(content: ft.Control, padding: int = 16, elevation: int = 1) -> ft.Card:
return ft.Card(
content=ft.Container(content=content, padding=padding),
elevation=elevation,
surface_tint_color=SURFACE,
)
def stat_card(title: str, value: str, icon, color: str = PRIMARY) -> ft.Container:
return ft.Container(
content=ft.Column(
[
ft.Row([ft.Icon(icon, color=color, size=28), body(title, color=TEXT_SECONDARY)], spacing=8),
ft.Text(value, size=32, weight=ft.FontWeight.BOLD, color=color),
],
spacing=8,
),
bgcolor=SURFACE,
border_radius=12,
padding=20,
shadow=ft.BoxShadow(spread_radius=0, blur_radius=8, color=ft.colors.with_opacity(0.08, ft.colors.BLACK)),
width=200,
)
# ─── Статус-бейджи ────────────────────────────────────────────────────────────
def status_badge(status: str) -> ft.Container:
cfg = {
"published": (SUCCESS, "Опубликовано"),
"draft": (TEXT_SECONDARY, "Черновик"),
}
color, label = cfg.get(status, (TEXT_SECONDARY, status))
return ft.Container(
content=ft.Text(label, size=11, color=ft.colors.WHITE, weight=ft.FontWeight.W_500),
bgcolor=color,
border_radius=4,
padding=ft.padding.symmetric(horizontal=8, vertical=3),
)
# ─── Разделитель ─────────────────────────────────────────────────────────────
def divider() -> ft.Divider:
return ft.Divider(height=1, color=DIVIDER)
# ─── Загрузчик ───────────────────────────────────────────────────────────────
def loader(size: int = 40) -> ft.ProgressRing:
return ft.ProgressRing(width=size, height=size, stroke_width=3, color=PRIMARY)
# ─── Снэкбар ─────────────────────────────────────────────────────────────────
def snack(page: ft.Page, message: str, error: bool = False):
page.snack_bar = ft.SnackBar(
content=ft.Text(message, color=ft.colors.WHITE),
bgcolor=ERROR if error else SUCCESS,
duration=3000,
)
page.snack_bar.open = True
page.update()
# ─── Диалог подтверждения ─────────────────────────────────────────────────────
def confirm_dialog(page: ft.Page, title: str, message: str, on_confirm) -> ft.AlertDialog:
dlg = ft.AlertDialog(
modal=True,
title=ft.Text(title, weight=ft.FontWeight.BOLD),
content=ft.Text(message),
actions=[
ft.TextButton("Отмена", on_click=lambda e: _close_dlg(page, dlg)),
btn_danger("Удалить", on_click=lambda e: (_close_dlg(page, dlg), on_confirm())),
],
)
return dlg
def _close_dlg(page: ft.Page, dlg: ft.AlertDialog):
dlg.open = False
page.update()
# ─── Пустое состояние ────────────────────────────────────────────────────────
def empty_state(icon, message: str) -> ft.Column:
return ft.Column(
[ft.Icon(icon, size=64, color=DIVIDER), body(message, color=TEXT_SECONDARY)],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
spacing=12,
)