feat(create): history viewer — revision timeline display (#40)
All checks were successful
Build and Test / build (pull_request) Successful in 29m36s
All checks were successful
Build and Test / build (pull_request) Successful in 29m36s
Add SiloHistoryViewer widget that opens in an MDI subwindow when the user double-clicks the History node in the Silo tree. Displays revision cards newest-first with revision number, Catppuccin-themed lifecycle status badges, author, timestamp, and commit comment. Changes: - silo_viewers.py: SiloHistoryViewer with revision card layout, status badge QSS, scroll area, empty-history placeholder Closes #40
This commit is contained in:
@@ -423,6 +423,131 @@ class SiloMetadataEditor(QtWidgets.QWidget):
|
||||
event.accept()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# History Viewer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_BADGE_STYLES = {
|
||||
"draft": "background: #45475a; color: #cdd6f4;",
|
||||
"review": "background: #f9e2af; color: #11111b;",
|
||||
"released": "background: #a6e3a1; color: #11111b;",
|
||||
"obsolete": "background: #f38ba8; color: #11111b;",
|
||||
}
|
||||
|
||||
|
||||
class SiloHistoryViewer(QtWidgets.QWidget):
|
||||
"""Read-only revision timeline from ``silo/history.json``."""
|
||||
|
||||
WINDOW_TITLE = "Silo \u2014 History"
|
||||
|
||||
def __init__(self, obj, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName(f"SiloViewer_{obj.Name}")
|
||||
self._build_ui(obj.RawContent)
|
||||
|
||||
def _build_ui(self, raw_content):
|
||||
try:
|
||||
data = json.loads(raw_content)
|
||||
except Exception:
|
||||
data = {}
|
||||
revisions = data.get("revisions", [])
|
||||
|
||||
outer = QtWidgets.QVBoxLayout(self)
|
||||
outer.setContentsMargins(16, 16, 16, 16)
|
||||
outer.setSpacing(12)
|
||||
|
||||
# Header
|
||||
header = QtWidgets.QHBoxLayout()
|
||||
title = QtWidgets.QLabel("Revision History")
|
||||
font = title.font()
|
||||
font.setPointSize(font.pointSize() + 2)
|
||||
font.setBold(True)
|
||||
title.setFont(font)
|
||||
header.addWidget(title)
|
||||
header.addStretch()
|
||||
outer.addLayout(header)
|
||||
|
||||
# Separator
|
||||
line = QtWidgets.QFrame()
|
||||
line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
outer.addWidget(line)
|
||||
|
||||
if not revisions:
|
||||
placeholder = QtWidgets.QLabel("No revision history available.")
|
||||
placeholder.setAlignment(QtCore.Qt.AlignCenter)
|
||||
outer.addWidget(placeholder)
|
||||
outer.addStretch()
|
||||
return
|
||||
|
||||
# Scroll area with revision cards
|
||||
scroll = QtWidgets.QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
content = QtWidgets.QWidget()
|
||||
cards_layout = QtWidgets.QVBoxLayout(content)
|
||||
cards_layout.setContentsMargins(0, 0, 0, 0)
|
||||
cards_layout.setSpacing(0)
|
||||
|
||||
for i, rev in enumerate(revisions):
|
||||
cards_layout.addWidget(self._make_revision_card(rev))
|
||||
if i < len(revisions) - 1:
|
||||
sep = QtWidgets.QFrame()
|
||||
sep.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
sep.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
cards_layout.addWidget(sep)
|
||||
|
||||
cards_layout.addStretch()
|
||||
scroll.setWidget(content)
|
||||
outer.addWidget(scroll, 1)
|
||||
|
||||
def _make_revision_card(self, rev):
|
||||
"""Build a widget for a single revision entry."""
|
||||
card = QtWidgets.QWidget()
|
||||
layout = QtWidgets.QVBoxLayout(card)
|
||||
layout.setContentsMargins(4, 8, 4, 8)
|
||||
layout.setSpacing(4)
|
||||
|
||||
# Top line: Rev N · status badge · author · timestamp
|
||||
top = QtWidgets.QHBoxLayout()
|
||||
top.setSpacing(8)
|
||||
|
||||
rev_num = rev.get("revision", "?")
|
||||
rev_label = QtWidgets.QLabel(f"<b>Rev {rev_num}</b>")
|
||||
top.addWidget(rev_label)
|
||||
|
||||
status = rev.get("status", "draft")
|
||||
badge = QtWidgets.QLabel(status)
|
||||
style = _BADGE_STYLES.get(status, _BADGE_STYLES["draft"])
|
||||
badge.setStyleSheet(
|
||||
f"QLabel {{ {style} border-radius: 4px; "
|
||||
f"padding: 1px 6px; font-size: 11px; }}"
|
||||
)
|
||||
top.addWidget(badge)
|
||||
|
||||
author = rev.get("author", "")
|
||||
if author:
|
||||
top.addWidget(QtWidgets.QLabel(f"\u00b7 {author}"))
|
||||
|
||||
timestamp = rev.get("timestamp", "")
|
||||
if timestamp:
|
||||
display_ts = _format_value("created_at", timestamp)
|
||||
top.addWidget(QtWidgets.QLabel(f"\u00b7 {display_ts}"))
|
||||
|
||||
top.addStretch()
|
||||
layout.addLayout(top)
|
||||
|
||||
# Comment line
|
||||
comment = rev.get("comment", "")
|
||||
if comment:
|
||||
comment_label = QtWidgets.QLabel(comment)
|
||||
comment_label.setWordWrap(True)
|
||||
comment_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||
layout.addWidget(comment_label)
|
||||
|
||||
return card
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Viewer factory
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -430,6 +555,7 @@ class SiloMetadataEditor(QtWidgets.QWidget):
|
||||
_VIEWER_REGISTRY = {
|
||||
"silo/manifest.json": SiloManifestViewer,
|
||||
"silo/metadata.json": SiloMetadataEditor,
|
||||
"silo/history.json": SiloHistoryViewer,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user