feat: AR-VMS-Seaman initial commit — Python FastAPI + PySide6 (runtime server + desktop studio client)
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
"""Welcome screen del Studio.
|
||||
|
||||
Pantalla inicial cuando no hay proyecto abierto. Muestra al usuario las dos
|
||||
acciones principales (crear nuevo / abrir existente) como botones grandes,
|
||||
más una lista de proyectos recientes y enlaces a docs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from vmssailor.studio.theme import (
|
||||
C_ABYSS,
|
||||
C_CYAN,
|
||||
C_CYAN_DEEP,
|
||||
C_FOAM,
|
||||
C_FOG,
|
||||
C_HORIZON,
|
||||
C_MIDNIGHT,
|
||||
C_SAND,
|
||||
C_STEEL,
|
||||
display_font,
|
||||
mono_font,
|
||||
ui_font,
|
||||
)
|
||||
|
||||
|
||||
BRAND_ROOT = Path(__file__).resolve().parents[2] / "docs" / "brand"
|
||||
|
||||
|
||||
class WelcomeScreen(QWidget):
|
||||
"""Pantalla de bienvenida con CTAs grandes."""
|
||||
|
||||
newProjectRequested = Signal()
|
||||
openProjectRequested = Signal()
|
||||
openRecentRequested = Signal(str) # path
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("welcomeScreen")
|
||||
self.setStyleSheet(
|
||||
f"#welcomeScreen {{ background: {C_ABYSS}; }}"
|
||||
)
|
||||
|
||||
outer = QVBoxLayout(self)
|
||||
outer.setContentsMargins(0, 0, 0, 0)
|
||||
outer.setSpacing(0)
|
||||
outer.addStretch(1)
|
||||
|
||||
center = QWidget()
|
||||
center.setMaximumWidth(880)
|
||||
cl = QVBoxLayout(center)
|
||||
cl.setSpacing(0)
|
||||
cl.setContentsMargins(48, 0, 48, 0)
|
||||
|
||||
# Hero with logo + title
|
||||
hero = QHBoxLayout()
|
||||
hero.setSpacing(24)
|
||||
|
||||
logo_path = BRAND_ROOT / "logo-mark.svg"
|
||||
if logo_path.exists():
|
||||
logo = QLabel()
|
||||
pix = QIcon(str(logo_path)).pixmap(120, 120)
|
||||
logo.setPixmap(pix)
|
||||
hero.addWidget(logo)
|
||||
|
||||
title_box = QVBoxLayout()
|
||||
title_box.setSpacing(4)
|
||||
title = QLabel("VMS-Sailor Studio")
|
||||
title.setFont(display_font(36, 700))
|
||||
title.setStyleSheet(f"color: {C_FOAM};")
|
||||
title_box.addWidget(title)
|
||||
sub = QLabel("VESSEL · MANAGEMENT · SYSTEM")
|
||||
sub.setFont(ui_font(11))
|
||||
sub.setStyleSheet(f"color: {C_HORIZON}; letter-spacing: 4px;")
|
||||
title_box.addWidget(sub)
|
||||
body = QLabel(
|
||||
"Herramienta de ingeniería para configurar el VMS-Sailor de cada buque. "
|
||||
"Crea un nuevo proyecto desde el wizard o abre un .vmsproj existente."
|
||||
)
|
||||
body.setWordWrap(True)
|
||||
body.setFont(ui_font(11))
|
||||
body.setStyleSheet(f"color: {C_SAND}; margin-top: 12px;")
|
||||
title_box.addWidget(body)
|
||||
title_box.addStretch(1)
|
||||
hero.addLayout(title_box, 1)
|
||||
cl.addLayout(hero)
|
||||
cl.addSpacing(32)
|
||||
|
||||
# Two big CTAs
|
||||
ctas = QHBoxLayout()
|
||||
ctas.setSpacing(16)
|
||||
ctas.addWidget(
|
||||
self._make_cta(
|
||||
title="Nuevo desde wizard",
|
||||
subtitle="Wizard de 8 pasos: tipo de buque, plantilla, dimensiones,\n"
|
||||
"sistemas, equipos, topología, confirmación.",
|
||||
signal=self.newProjectRequested,
|
||||
primary=True,
|
||||
shortcut="Ctrl+N",
|
||||
),
|
||||
1,
|
||||
)
|
||||
ctas.addWidget(
|
||||
self._make_cta(
|
||||
title="Abrir proyecto",
|
||||
subtitle="Carga un archivo .vmsproj existente desde disco.\n"
|
||||
"Edita equipos, mímicos, tags y alarmas.",
|
||||
signal=self.openProjectRequested,
|
||||
primary=False,
|
||||
shortcut="Ctrl+O",
|
||||
),
|
||||
1,
|
||||
)
|
||||
cl.addLayout(ctas)
|
||||
cl.addSpacing(28)
|
||||
|
||||
# Quick links / docs
|
||||
links_label = QLabel("DOCUMENTACIÓN")
|
||||
links_label.setFont(ui_font(9))
|
||||
links_label.setStyleSheet(
|
||||
f"color: {C_FOG}; letter-spacing: 2.5px; margin-bottom: 8px;"
|
||||
)
|
||||
cl.addWidget(links_label)
|
||||
|
||||
links_row = QHBoxLayout()
|
||||
links_row.setSpacing(8)
|
||||
for txt, hint in (
|
||||
("Sprint 0 — Fundaciones", "docs/architecture.md"),
|
||||
("Sistema de coordenadas", "docs/coords.md"),
|
||||
("Design System", "docs/design_system.md"),
|
||||
("Mockups visuales", "docs/mockups/index.html"),
|
||||
):
|
||||
chip = QLabel(f" {txt} ")
|
||||
chip.setFont(mono_font(9))
|
||||
chip.setStyleSheet(
|
||||
f"color: {C_SAND}; background: {C_MIDNIGHT}; "
|
||||
f"border: 1px solid {C_STEEL}; border-radius: 12px; padding: 4px 10px;"
|
||||
)
|
||||
chip.setToolTip(hint)
|
||||
links_row.addWidget(chip)
|
||||
links_row.addStretch(1)
|
||||
cl.addLayout(links_row)
|
||||
|
||||
outer.addWidget(center, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||
outer.addStretch(2)
|
||||
|
||||
footer = QLabel(
|
||||
"Propiedad intelectual de Álvaro · El Studio no se distribuye al cliente"
|
||||
)
|
||||
footer.setFont(mono_font(9))
|
||||
footer.setStyleSheet(f"color: {C_FOG}; padding: 16px;")
|
||||
footer.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
outer.addWidget(footer)
|
||||
|
||||
def _make_cta(
|
||||
self,
|
||||
title: str,
|
||||
subtitle: str,
|
||||
signal,
|
||||
primary: bool,
|
||||
shortcut: str = "",
|
||||
) -> QFrame:
|
||||
frame = QFrame()
|
||||
frame.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
bg_hover = C_CYAN_DEEP if primary else C_STEEL
|
||||
border = C_CYAN if primary else C_IRON_FALLBACK
|
||||
bg = C_MIDNIGHT if not primary else "rgba(0,217,255,0.08)"
|
||||
frame.setStyleSheet(
|
||||
f"""
|
||||
QFrame {{
|
||||
background: {bg};
|
||||
border: 1px solid {border};
|
||||
border-radius: 14px;
|
||||
min-height: 180px;
|
||||
}}
|
||||
QFrame:hover {{
|
||||
background: {bg_hover};
|
||||
border-color: {C_CYAN};
|
||||
}}
|
||||
"""
|
||||
)
|
||||
lay = QVBoxLayout(frame)
|
||||
lay.setContentsMargins(24, 22, 24, 22)
|
||||
lay.setSpacing(8)
|
||||
|
||||
t = QLabel(title)
|
||||
t.setFont(display_font(22, 700))
|
||||
t.setStyleSheet(f"color: {C_FOAM};")
|
||||
lay.addWidget(t)
|
||||
|
||||
s = QLabel(subtitle)
|
||||
s.setFont(ui_font(10))
|
||||
s.setStyleSheet(f"color: {C_FOG};")
|
||||
s.setWordWrap(True)
|
||||
lay.addWidget(s)
|
||||
|
||||
lay.addStretch(1)
|
||||
kbd_row = QHBoxLayout()
|
||||
kbd_row.addStretch(1)
|
||||
if shortcut:
|
||||
kbd = QLabel(shortcut)
|
||||
kbd.setFont(mono_font(9))
|
||||
kbd.setStyleSheet(
|
||||
f"color: {C_FOG}; background: {C_ABYSS}; "
|
||||
f"border: 1px solid {C_STEEL}; border-radius: 4px; padding: 2px 8px;"
|
||||
)
|
||||
kbd_row.addWidget(kbd)
|
||||
arrow = QLabel("→")
|
||||
arrow.setFont(display_font(22, 700))
|
||||
arrow.setStyleSheet(f"color: {C_CYAN}; margin-left: 8px;")
|
||||
kbd_row.addWidget(arrow)
|
||||
lay.addLayout(kbd_row)
|
||||
|
||||
frame.mousePressEvent = lambda _ev: signal.emit() # type: ignore[method-assign]
|
||||
return frame
|
||||
|
||||
|
||||
# Fallback color para borde no-primary (no exportado en theme aún si quieres)
|
||||
C_IRON_FALLBACK = "#2C3E5C"
|
||||
Reference in New Issue
Block a user