test ferst
This commit is contained in:
252
web/designer.py
Normal file
252
web/designer.py
Normal file
@@ -0,0 +1,252 @@
|
||||
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 = "#1565C0"
|
||||
PRIMARY_LIGHT = "#1976D2"
|
||||
PRIMARY_DARK = "#0D47A1"
|
||||
ACCENT = "#FF6D00"
|
||||
SUCCESS = "#2E7D32"
|
||||
WARNING = "#F57F17"
|
||||
ERROR = "#C62828"
|
||||
SURFACE = "#FFFFFF"
|
||||
BACKGROUND = "#F5F5F5"
|
||||
TEXT_PRIMARY = "#212121"
|
||||
TEXT_SECONDARY = "#616161"
|
||||
DIVIDER = "#E0E0E0"
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user