diff --git a/graphlink_app/graphlink_canvas/graphlink_canvas_base.py b/graphlink_app/graphlink_canvas/graphlink_canvas_base.py index 34ec440..c38de02 100644 --- a/graphlink_app/graphlink_canvas/graphlink_canvas_base.py +++ b/graphlink_app/graphlink_canvas/graphlink_canvas_base.py @@ -4,7 +4,7 @@ from PySide6.QtCore import Qt, QRectF, QTimer, Signal from PySide6.QtGui import QPainter, QColor, QBrush, QPen -from graphlink_config import get_current_palette +from graphlink_config import get_current_palette, get_surface_color class HoverAnimationMixin: @@ -145,19 +145,19 @@ def __init__(self, parent=None): self.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) self.setStyleSheet( - """ - QLineEdit { + f""" + QLineEdit {{ background-color: rgba(26, 26, 26, 0.96); - color: #f1f1f1; - border: 1px solid #5a5a5a; + color: {get_surface_color("text_strong")}; + border: 1px solid {get_surface_color("handle")}; border-radius: 5px; padding: 4px 8px; - selection-background-color: #5a5a5a; - selection-color: #ffffff; - } - QLineEdit:focus { - border-color: #7a7a7a; - } + selection-background-color: {get_surface_color("handle")}; + selection-color: {get_surface_color("text_bright")}; + }} + QLineEdit:focus {{ + border-color: {get_surface_color("text_muted")}; + }} """ ) self.editingFinished.connect(self._emit_commit_if_needed) diff --git a/graphlink_app/graphlink_canvas/graphlink_canvas_chart_item.py b/graphlink_app/graphlink_canvas/graphlink_canvas_chart_item.py index e15b325..d53d553 100644 --- a/graphlink_app/graphlink_canvas/graphlink_canvas_chart_item.py +++ b/graphlink_app/graphlink_canvas/graphlink_canvas_chart_item.py @@ -19,9 +19,10 @@ from PySide6.QtCore import Qt, QRectF, QPointF, QSizeF, QTimer, QStandardPaths from PySide6.QtGui import QPainter, QColor, QBrush, QPen, QFont, QPainterPath, QImage, QLinearGradient -from graphlink_config import get_current_palette, get_graph_node_colors +from graphlink_config import get_current_palette, get_graph_node_colors, get_surface_color from graphlink_chart_data import ChartDataError, canonicalize_chart_data from graphlink_context_menu import create_context_menu +from graphlink_styles import FONT_FAMILY_NAME class ChartItem(QGraphicsItem): @@ -79,9 +80,9 @@ def __init__(self, data, pos, parent=None, parent_content_node=None): self.aspect_ratio_locked = True self._base_aspect_ratio = self.DEFAULT_WIDTH / self.DEFAULT_HEIGHT self.resize_start_aspect_ratio = self._base_aspect_ratio - self.font_family = "Segoe UI" + self.font_family = FONT_FAMILY_NAME self.font_size = 10 - self.font_color = QColor("#DDDDDD") + self.font_color = QColor(get_surface_color("text_primary")) self.figure = Figure(figsize=(6, 4), dpi=160) self.canvas = FigureCanvasAgg(self.figure) @@ -216,12 +217,12 @@ def _mpl_rgba(color, alpha=1.0): return (color.redF(), color.greenF(), color.blueF(), alpha) def _build_theme(self, palette): - surface = QColor("#1B1B1B") - panel = QColor("#151515") - border = QColor("#474747") - text = QColor("#F7F7F7") - muted = QColor("#A5A5A5") - grid = QColor("#9A9A9A") + surface = QColor(get_surface_color("window")) + panel = QColor(get_surface_color("inset_deep")) + border = QColor(get_surface_color("divider")) + text = QColor(get_surface_color("text_bright")) + muted = QColor(get_surface_color("text_label")) + grid = QColor(get_surface_color("text_label")) primary = QColor(palette.AI_NODE) secondary = QColor(palette.USER_NODE) selection = QColor(palette.SELECTION) @@ -810,7 +811,7 @@ def _render_chart_content(self, theme): return self.chart_image def update_font_settings(self, family, size, color): - self.font_family = str(family or "Segoe UI") + self.font_family = str(family or FONT_FAMILY_NAME) self.font_size = max(6, min(32, int(size))) self.font_color = QColor(color) self.generate_chart() @@ -925,7 +926,7 @@ def paint(self, painter, option, widget=None): painter.drawText(header_rect.adjusted(12, 0, -120, 0), Qt.AlignmentFlag.AlignVCenter, self.title) badge_rect = QRectF(self.width - 102, 8, 90, 24) - painter.setPen(QPen(self._with_alpha(QColor("#FFFFFF"), 65), 1)) + painter.setPen(QPen(self._with_alpha(QColor(get_surface_color("text_bright")), 65), 1)) painter.setBrush(QBrush(node_colors["badge_fill"])) painter.drawRoundedRect(badge_rect, 12, 12) painter.setPen(QPen(self.font_color)) @@ -943,7 +944,7 @@ def paint(self, painter, option, widget=None): if self.hovered or self.isSelected(): handle_size = 10 handle_rect = QRectF(self.width - handle_size, self.height - handle_size, handle_size, handle_size) - painter.setPen(QPen(QColor("#FFFFFF"))) + painter.setPen(QPen(QColor(get_surface_color("text_bright")))) painter.drawLine(handle_rect.topLeft(), handle_rect.bottomRight()) painter.drawLine( handle_rect.topLeft() + QPointF(0, handle_size / 2), diff --git a/graphlink_app/graphlink_canvas/graphlink_canvas_container.py b/graphlink_app/graphlink_canvas/graphlink_canvas_container.py index 1a612bb..e1b2df7 100644 --- a/graphlink_app/graphlink_canvas/graphlink_canvas_container.py +++ b/graphlink_app/graphlink_canvas/graphlink_canvas_container.py @@ -8,7 +8,15 @@ from .graphlink_canvas_base import CanvasHeaderLineEdit, GhostFrame, update_connections_for_items from .graphlink_canvas_dialogs import ColorPickerDialog -from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors +from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_surface_color + +# The container's user-facing body color DEFAULT. Persisted scene DATA (saved +# into and restored from chat JSON, see graphlink_session/serializers + +# deserializers), not theme chrome - deliberately NOT a THEME_TOKENS lookup: +# a saved container must keep its color across theme switches, exactly like +# DEFAULT_GRID_COLOR in graphlink_grid_view_settings. The one allowed literal +# home for this value (see tests/test_ui_token_acceptance.py's allowlist). +DEFAULT_CONTAINER_COLOR = "#3a3a3a" class Container(QGraphicsItem): @@ -46,7 +54,7 @@ def __init__(self, items, parent=None): self.expanded_rect = QRectF() # Caches the size before collapsing. self.rect = QRectF() - self.color = "#3a3a3a" + self.color = DEFAULT_CONTAINER_COLOR self.header_color = None # Rects for hover detection of UI buttons in the header. @@ -351,7 +359,7 @@ def paint(self, painter, option, widget=None): self.collapse_button_rect = QRectF(self.rect.right() - 34, self.rect.top() + 10, 24, 24) # Draw the title text in collapsed mode. - painter.setPen(QColor("#ffffff")) + painter.setPen(QColor(get_surface_color("text_bright"))) font = canvas_font(self.scene(), delta=2, weight=QFont.Weight.Bold) painter.setFont(font) title_rect = QRectF(self.rect.left() + 14, self.rect.top(), self.rect.width() - 56, self.rect.height()) @@ -360,7 +368,7 @@ def paint(self, painter, option, widget=None): expand_icon = qta.icon( 'fa5s.expand-arrows-alt', - color='#ffffff' if self.collapse_button_hovered or self.hovered else '#9a9a9a', + color=get_surface_color("text_bright") if self.collapse_button_hovered or self.hovered else get_surface_color("text_label"), ) expand_icon.paint(painter, self.collapse_button_rect.adjusted(3, 3, -3, -3).toRect()) return @@ -407,7 +415,7 @@ def paint(self, painter, option, widget=None): icon.paint(painter, self.collapse_button_rect.adjusted(4, 4, -4, -4).toRect()) # Draw Color Button - painter.setPen(QPen(QColor("#ffffff") if self.color_button_hovered else node_colors["border"])) + painter.setPen(QPen(QColor(get_surface_color("text_bright")) if self.color_button_hovered else node_colors["border"])) painter.setBrush(QBrush(header_base_color)) painter.drawEllipse(self.color_button_rect) @@ -419,7 +427,7 @@ def paint(self, painter, option, widget=None): painter.drawEllipse(center + QPointF(6, 0), 2, 2) # Draw the title, either in display or editing mode. - painter.setPen(QPen(QColor("#ffffff"))) + painter.setPen(QPen(QColor(get_surface_color("text_bright")))) font = canvas_font(self.scene(), weight=QFont.Weight.Bold) painter.setFont(font) text_rect = self._header_text_rect() @@ -522,7 +530,7 @@ def show_color_picker(self): if dialog.exec() == QDialog.DialogCode.Accepted: color, color_type = dialog.get_selected_color() if color_type == "default": - self.color = "#3a3a3a" + self.color = DEFAULT_CONTAINER_COLOR self.header_color = None elif color_type == "full": self.color = color diff --git a/graphlink_app/graphlink_canvas/graphlink_canvas_dialogs.py b/graphlink_app/graphlink_canvas/graphlink_canvas_dialogs.py index f347671..b901efc 100644 --- a/graphlink_app/graphlink_canvas/graphlink_canvas_dialogs.py +++ b/graphlink_app/graphlink_canvas/graphlink_canvas_dialogs.py @@ -5,7 +5,7 @@ QDialog, QVBoxLayout, QWidget, QHBoxLayout, QLineEdit, QLabel, QPushButton, QGraphicsDropShadowEffect, QTextEdit, QGridLayout, QApplication ) -from graphlink_config import get_current_palette, get_semantic_color +from graphlink_config import get_current_palette, get_semantic_color, get_surface_color class ColorPickerDialog(QDialog): """ @@ -74,7 +74,7 @@ def __init__(self, parent=None): def create_section(title, color_type, names_list): label = QLabel(title) - label.setStyleSheet("color: #CCCCCC; font-size: 10px; margin-top: 5px;") + label.setStyleSheet(f"color: {get_surface_color('text_soft')}; font-size: 10px; margin-top: 5px;") main_layout.addWidget(label) grid_layout = QGridLayout() @@ -91,18 +91,18 @@ def create_section(title, color_type, names_list): btn.setCursor(Qt.CursorShape.PointingHandCursor) style = f""" - QPushButton {{ background-color: {color_data["color"]}; border: 2px solid #3F3F3F; border-radius: 14px; }} - QPushButton:hover {{ border: 2px solid #FFFFFF; }} + QPushButton {{ background-color: {color_data["color"]}; border: 2px solid {get_surface_color("border")}; border-radius: 14px; }} + QPushButton:hover {{ border: 2px solid {get_surface_color("text_bright")}; }} """ if color_type == "header": style = f""" QPushButton {{ background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {color_data["color"]}, stop:0.4 {color_data["color"]}, - stop:0.41 #3F3F3F, stop:1 #3F3F3F); - border: 2px solid #3F3F3F; border-radius: 14px; + stop:0.41 {get_surface_color("border")}, stop:1 {get_surface_color("border")}); + border: 2px solid {get_surface_color("border")}; border-radius: 14px; }} - QPushButton:hover {{ border: 2px solid #FFFFFF; }} + QPushButton:hover {{ border: 2px solid {get_surface_color("text_bright")}; }} """ btn.setStyleSheet(style) btn.setProperty("frame_color_data", color_data) @@ -124,20 +124,20 @@ def create_section(title, color_type, names_list): main_layout.addStretch() - self.setStyleSheet(""" - QDialog { background: transparent; } - QWidget#colorPickerContainer { background-color: #252525; border-radius: 8px; } - QLabel#colorPickerTitle { color: #FFFFFF; font-size: 12px; font-weight: bold; } - QPushButton { background-color: #3F3F3F; border-radius: 5px; padding: 8px; } - QPushButton:hover { background-color: #555555; } - QPushButton#colorPickerCloseButton { + self.setStyleSheet(f""" + QDialog {{ background: transparent; }} + QWidget#colorPickerContainer {{ background-color: {get_surface_color("node_body")}; border-radius: 8px; }} + QLabel#colorPickerTitle {{ color: {get_surface_color("text_bright")}; font-size: 12px; font-weight: bold; }} + QPushButton {{ background-color: {get_surface_color("border")}; border-radius: 5px; padding: 8px; }} + QPushButton:hover {{ background-color: {get_surface_color("handle")}; }} + QPushButton#colorPickerCloseButton {{ background-color: transparent; border-radius: 12px; padding: 0px; - } - QPushButton#colorPickerCloseButton:hover { - background-color: #4A4A4A; - } + }} + QPushButton#colorPickerCloseButton:hover {{ + background-color: {get_surface_color("border_strong")}; + }} """) self.selected_color = None diff --git a/graphlink_app/graphlink_canvas/graphlink_canvas_frame.py b/graphlink_app/graphlink_canvas/graphlink_canvas_frame.py index 926efa3..640e075 100644 --- a/graphlink_app/graphlink_canvas/graphlink_canvas_frame.py +++ b/graphlink_app/graphlink_canvas/graphlink_canvas_frame.py @@ -10,7 +10,7 @@ from .graphlink_canvas_base import CanvasHeaderLineEdit, update_connections_for_items from .graphlink_canvas_dialogs import ColorPickerDialog -from graphlink_config import canvas_font, get_current_palette, get_semantic_color +from graphlink_config import canvas_font, get_current_palette, get_semantic_color, get_surface_color class Frame(QGraphicsItem): @@ -47,8 +47,8 @@ def __init__(self, nodes, parent=None): # Load icons for the lock/unlock button. # QtAwesome's legacy ``fa`` prefix is not installed in current releases; # use the Font Awesome 5 solid set used by the rest of the canvas. - self.lock_icon = qta.icon('fa5s.lock', color='#ffffff') - self.unlock_icon = qta.icon('fa5s.unlock-alt', color='#ffffff') + self.lock_icon = qta.icon('fa5s.lock', color=get_surface_color("text_bright")) + self.unlock_icon = qta.icon('fa5s.unlock-alt', color=get_surface_color("text_bright")) self.lock_icon_hover = qta.icon('fa5s.lock', color=get_semantic_color("status_info").name()) self.unlock_icon_hover = qta.icon('fa5s.unlock-alt', color=get_semantic_color("status_success").name()) @@ -57,7 +57,7 @@ def __init__(self, nodes, parent=None): self.is_collapsed = False self.expanded_rect = QRectF() self.rect = QRectF() - self.color = "#2d2d2d" + self.color = get_surface_color("node_body") self.header_color = None self.collapse_button_rect = QRectF(0, 0, 24, 24) @@ -557,7 +557,7 @@ def show_color_picker(self): if dialog.exec() == QDialog.DialogCode.Accepted: color, color_type = dialog.get_selected_color() if color_type == "default": - self.color = "#2d2d2d" + self.color = get_surface_color("node_body") self.header_color = None elif color_type == "full": self.color = color @@ -615,7 +615,7 @@ def paint(self, painter, option, widget=None): if self.is_collapsed: base_color = QColor(self.color) - outline_color = palette.SELECTION if self.isSelected() else palette.AI_NODE if self.hovered else QColor("#555555") + outline_color = palette.SELECTION if self.isSelected() else palette.AI_NODE if self.hovered else QColor(get_surface_color("handle")) path = QPainterPath() path.addRoundedRect(self.rect, 10, 10) @@ -627,7 +627,7 @@ def paint(self, painter, option, widget=None): self.lock_button_rect = QRectF() self.color_button_rect = QRectF() - painter.setPen(QPen(QColor("#ffffff"))) + painter.setPen(QPen(QColor(get_surface_color("text_bright")))) font = canvas_font(self.scene(), delta=1, weight=QFont.Weight.Bold) painter.setFont(font) title_rect = QRectF(self.rect.left() + 14, self.rect.top(), self.rect.width() - 56, self.rect.height()) @@ -636,7 +636,7 @@ def paint(self, painter, option, widget=None): expand_icon = qta.icon( 'fa5s.expand-arrows-alt', - color='#ffffff' if self.collapse_button_hovered or self.hovered else '#9a9a9a', + color=get_surface_color("text_bright") if self.collapse_button_hovered or self.hovered else get_surface_color("text_label"), ) expand_icon.paint(painter, self.collapse_button_rect.adjusted(3, 3, -3, -3).toRect()) return @@ -653,7 +653,7 @@ def paint(self, painter, option, widget=None): elif self.hovered: outline_color = palette.AI_NODE else: - outline_color = QColor("#555555") + outline_color = QColor(get_surface_color("handle")) path = QPainterPath() path.addRoundedRect(self.rect, 10, 10) @@ -688,19 +688,19 @@ def paint(self, painter, option, widget=None): # Draw collapse button. self.collapse_button_rect = QRectF(self.rect.right() - 102, self.rect.top() + 8, 24, 24) - painter.setPen(QPen(QColor("#ffffff") if self.collapse_button_hovered else QColor("#555555"))) - painter.setBrush(QBrush(QColor("#3f3f3f"))) + painter.setPen(QPen(QColor(get_surface_color("text_bright")) if self.collapse_button_hovered else QColor(get_surface_color("handle")))) + painter.setBrush(QBrush(QColor(get_surface_color("border")))) painter.drawEllipse(self.collapse_button_rect) collapse_icon = qta.icon( 'fa5s.compress-arrows-alt', - color='#ffffff' if self.collapse_button_hovered else '#bbbbbb', + color=get_surface_color("text_bright") if self.collapse_button_hovered else get_surface_color("text_secondary"), ) collapse_icon.paint(painter, self.collapse_button_rect.adjusted(4, 4, -4, -4).toRect()) # Draw lock button. self.lock_button_rect = QRectF(self.rect.right() - 68, self.rect.top() + 8, 24, 24) - painter.setPen(QPen(palette.USER_NODE if self.lock_button_hovered else QColor("#555555"))) - painter.setBrush(QBrush(QColor("#3f3f3f"))) + painter.setPen(QPen(palette.USER_NODE if self.lock_button_hovered else QColor(get_surface_color("handle")))) + painter.setBrush(QBrush(QColor(get_surface_color("border")))) painter.drawEllipse(self.lock_button_rect) icon = self.lock_icon_hover if self.is_locked and self.lock_button_hovered else self.lock_icon if self.is_locked else self.unlock_icon_hover if self.lock_button_hovered else self.unlock_icon icon_size = 18 @@ -711,10 +711,10 @@ def paint(self, painter, option, widget=None): # Draw color button. self.color_button_rect = QRectF(self.rect.right() - 34, self.rect.top() + 8, 24, 24) - painter.setPen(QPen(QColor("#ffffff") if self.color_button_hovered else QColor("#555555"))) + painter.setPen(QPen(QColor(get_surface_color("text_bright")) if self.color_button_hovered else QColor(get_surface_color("handle")))) painter.setBrush(QBrush(QColor(self.header_color if self.header_color else self.color))) painter.drawEllipse(self.color_button_rect) - icon_color = QColor("#ffffff"); icon_color.setAlpha(180) + icon_color = QColor(get_surface_color("text_bright")); icon_color.setAlpha(180) painter.setPen(QPen(icon_color)) circle_size, spacing = 4, 3 total_width = (circle_size * 3) + (spacing * 2) @@ -725,7 +725,7 @@ def paint(self, painter, option, widget=None): painter.drawEllipse(QRectF(x_pos, y_pos, circle_size, circle_size)) # Draw title (note). - painter.setPen(QPen(QColor("#ffffff"))) + painter.setPen(QPen(QColor(get_surface_color("text_bright")))) font = canvas_font(self.scene()) painter.setFont(font) text_rect = self._header_text_rect() diff --git a/graphlink_app/graphlink_canvas/graphlink_canvas_navigation_pin.py b/graphlink_app/graphlink_canvas/graphlink_canvas_navigation_pin.py index 33ce115..305ce1e 100644 --- a/graphlink_app/graphlink_canvas/graphlink_canvas_navigation_pin.py +++ b/graphlink_app/graphlink_canvas/graphlink_canvas_navigation_pin.py @@ -9,6 +9,9 @@ from PySide6.QtCore import Qt, QRectF, Signal from PySide6.QtGui import QPainter, QColor, QPen, QFont, QFontMetrics, QPainterPath +from graphlink_config import get_surface_color +from graphlink_styles import FONT_FAMILY_NAME + class NavigationPin(QGraphicsObject): """ @@ -75,15 +78,15 @@ def paint(self, painter, option, widget=None): selected = self.isSelected() active = selected or self.hovered - marker_fill = QColor("#c8c8c8" if selected else "#a0a0a0" if self.hovered else "#777777") - marker_border = QColor("#f2f2f2" if selected else "#bdbdbd" if self.hovered else "#555555") - surface = QColor("#252525") + marker_fill = QColor(get_surface_color("text_soft") if selected else get_surface_color("text_label") if self.hovered else get_surface_color("text_muted")) + marker_border = QColor(get_surface_color("text_strong") if selected else get_surface_color("text_secondary") if self.hovered else get_surface_color("handle")) + surface = QColor(get_surface_color("node_body")) # A beacon-style marker makes navigation pins visually distinct from # connection pins: rounded square body, inset waypoint core, stem, and # ground contact. It deliberately uses grayscale only. if active: - halo = QColor("#d8d8d8") + halo = QColor(get_surface_color("text_primary")) halo.setAlpha(35 if not selected else 55) painter.setPen(Qt.PenStyle.NoPen) painter.setBrush(halo) @@ -104,8 +107,8 @@ def paint(self, painter, option, widget=None): painter.drawEllipse(QRectF(-5.0, 25.0, 10.0, 7.0)) if active: - label_surface = QColor("#242424") - label_border = QColor("#777777" if selected else "#555555") + label_surface = QColor(get_surface_color("node_body")) + label_border = QColor(get_surface_color("text_muted") if selected else get_surface_color("handle")) painter.setPen(QPen(label_border, 1.0)) painter.setBrush(label_surface) painter.drawRoundedRect(self._LABEL_RECT, 10.0, 10.0) @@ -114,10 +117,10 @@ def paint(self, painter, option, widget=None): painter.setPen(QPen(label_border, 1.0)) painter.drawLine(0.0, self._LABEL_RECT.bottom(), 0.0, -15.0) - font = QFont("Segoe UI", 8) + font = QFont(FONT_FAMILY_NAME, 8) font.setWeight(QFont.Weight.DemiBold if selected else QFont.Weight.Normal) painter.setFont(font) - painter.setPen(QColor("#f0f0f0")) + painter.setPen(QColor(get_surface_color("text_strong"))) title = QFontMetrics(font).elidedText( str(self.title or "Waypoint"), Qt.TextElideMode.ElideRight, 142 ) diff --git a/graphlink_app/graphlink_canvas/graphlink_canvas_note.py b/graphlink_app/graphlink_canvas/graphlink_canvas_note.py index 33678bd..46b4417 100644 --- a/graphlink_app/graphlink_canvas/graphlink_canvas_note.py +++ b/graphlink_app/graphlink_canvas/graphlink_canvas_note.py @@ -12,7 +12,8 @@ ) from .graphlink_canvas_dialogs import ColorPickerDialog -from graphlink_config import canvas_font, get_current_palette +from graphlink_config import canvas_font, get_current_palette, get_surface_color +from graphlink_styles import FONT_FAMILY_NAME from graphlink_widgets import ScrollBar @@ -58,7 +59,7 @@ def __init__(self, pos, parent=None): # Geometry and appearance self.width = self.DEFAULT_WIDTH self.height = self.DEFAULT_HEIGHT - self.color = "#2d2d2d" + self.color = get_surface_color("node_body") self.header_color = None # State for in-place text editing @@ -119,7 +120,7 @@ def _setup_document(self): Configures the QTextDocument for rendering, applying styles from the scene and converting the Markdown content to HTML. """ - font_family, font_size, color = "Segoe UI", 10, "#dddddd" + font_family, font_size, color = FONT_FAMILY_NAME, 10, get_surface_color("text_primary") if self.scene(): font_family = self.scene().font_family @@ -128,7 +129,7 @@ def _setup_document(self): stylesheet = f""" p, ul, ol, li, blockquote {{ color: {color}; font-family: '{font_family}'; font-size: {font_size}pt; }} - pre {{ background-color: #1e1e1e; padding: 8px; border-radius: 4px; white-space: pre-wrap; font-family: Consolas, monospace; }} + pre {{ background-color: {get_surface_color("window")}; padding: 8px; border-radius: 4px; white-space: pre-wrap; font-family: Consolas, monospace; }} """ self.document.setDefaultStyleSheet(stylesheet) @@ -219,9 +220,9 @@ def paint(self, painter, option, widget=None): path = QPainterPath() path.addRoundedRect(0, 0, self.width, self.height, 10, 10) - pen = QPen(QColor("#555555")) + pen = QPen(QColor(get_surface_color("handle"))) if self.isSelected(): pen = QPen(palette.SELECTION, 2) - elif self.hovered: pen = QPen(QColor("#ffffff"), 2) + elif self.hovered: pen = QPen(QColor(get_surface_color("text_bright")), 2) # Special outline for system prompt notes. if self.is_system_prompt: @@ -231,8 +232,8 @@ def paint(self, painter, option, widget=None): painter.setPen(pen) gradient = QLinearGradient(QPointF(0, 0), QPointF(0, self.height)) - gradient.setColorAt(0, QColor("#4a4a4a")) - gradient.setColorAt(1, QColor("#2d2d2d")) + gradient.setColorAt(0, QColor(get_surface_color("border_strong"))) + gradient.setColorAt(1, QColor(get_surface_color("node_body"))) painter.setBrush(QBrush(gradient)) painter.drawPath(path) @@ -256,16 +257,16 @@ def paint(self, painter, option, widget=None): # Draw header icons for special note types. icon_rect = QRectF(10, (self.HEADER_HEIGHT - 16) / 2, 16, 16) if self.is_system_prompt: - qta.icon('fa5s.cog', color='#ffffff').paint(painter, icon_rect.toRect()) + qta.icon('fa5s.cog', color=get_surface_color("text_bright")).paint(painter, icon_rect.toRect()) elif self.is_summary_note: - qta.icon('fa5s.object-group', color='#ffffff').paint(painter, icon_rect.toRect()) + qta.icon('fa5s.object-group', color=get_surface_color("text_bright")).paint(painter, icon_rect.toRect()) # Draw the color picker button. self.color_button_rect = QRectF(self.width - 34, 8, 24, 24) - painter.setPen(QPen(QColor("#ffffff") if self.color_button_hovered else QColor("#555555"))) + painter.setPen(QPen(QColor(get_surface_color("text_bright")) if self.color_button_hovered else QColor(get_surface_color("handle")))) painter.setBrush(QBrush(header_base_color)) painter.drawEllipse(self.color_button_rect) - icon_color = QColor("#ffffff"); icon_color.setAlpha(180) + icon_color = QColor(get_surface_color("text_bright")); icon_color.setAlpha(180) painter.setPen(QPen(icon_color)) circle_size, spacing = 4, 3 total_width = (circle_size * 3) + (spacing * 2) @@ -276,7 +277,7 @@ def paint(self, painter, option, widget=None): painter.drawEllipse(QRectF(x_pos, y_pos, circle_size, circle_size)) # --- Content Rendering --- - painter.setPen(QPen(QColor("#ffffff"))) + painter.setPen(QPen(QColor(get_surface_color("text_bright")))) font = canvas_font(self.scene()) painter.setFont(font) diff --git a/graphlink_app/graphlink_config.py b/graphlink_app/graphlink_config.py index c4bf96c..8833f73 100644 --- a/graphlink_app/graphlink_config.py +++ b/graphlink_app/graphlink_config.py @@ -1,6 +1,6 @@ from PySide6.QtGui import QColor, QFont from PySide6.QtWidgets import QApplication -from graphlink_styles import THEME_TOKENS, THEMES +from graphlink_styles import FONT_FAMILY_NAME, THEME_TOKENS, THEMES CURRENT_THEME = "dark" @@ -15,15 +15,41 @@ def canvas_font(scene=None, delta=0, weight=QFont.Weight.Normal): reach their headers. Keeping this small helper in the shared config module makes those headers follow the same family and scale as document-backed nodes. """ - family = getattr(scene, "font_family", "Segoe UI") if scene else "Segoe UI" + family = getattr(scene, "font_family", FONT_FAMILY_NAME) if scene else FONT_FAMILY_NAME base_size = getattr(scene, "font_size", 10) if scene else 10 font = QFont(family, max(1, int(base_size) + int(delta)), weight) return font -def canvas_font_color(scene=None, fallback="#DDDDDD"): +def canvas_font_color(scene=None, fallback=None): color = getattr(scene, "font_color", None) if scene else None - return QColor(color) if color is not None else QColor(fallback) + if color is not None: + return QColor(color) + if fallback is not None: + return QColor(fallback) + # UI-refactor P0: the default falls back to the theme's primary text + # surface role instead of a hardcoded literal. + return QColor(get_surface_color("text_primary")) + + +def get_syntax_color(name: str): + """Look up a syntax-highlight role's hex string for the current theme + (keyword/builtin/number/string/comment/function). Sweep adjudication + moved PythonHighlighter's palette into THEME_TOKENS; this is its lookup.""" + return THEME_TOKENS[CURRENT_THEME]["syntax"][name] + + +def get_surface_color(name: str): + """Look up a neutral surface/text role's hex string for the current theme. + + UI-refactor P0 (doc/UI_QA_AUDIT.md section 7): the lookup the hex-literal + sweep migrated node/canvas/widget chrome onto. Mirrors + get_semantic_color()'s table-lookup shape but returns the raw hex string + rather than a QColor - the dominant call sites are f-string stylesheets, + and QColor construction is one wrap away for the painting sites that + need it.""" + tokens = THEME_TOKENS[CURRENT_THEME]["surface"] + return tokens[name] def is_monochrome_theme(): diff --git a/graphlink_app/graphlink_connections.py b/graphlink_app/graphlink_connections.py index 1c20dfb..ce6b93c 100644 --- a/graphlink_app/graphlink_connections.py +++ b/graphlink_app/graphlink_connections.py @@ -9,7 +9,7 @@ from graphlink_canvas.graphlink_canvas_base import iter_scene_connection_lists from graphlink_canvas_items import Container, Frame, Note -from graphlink_config import get_current_palette +from graphlink_config import get_current_palette, get_surface_color from graphlink_pycoder import PyCoderNode from graphlink_conversation_node import ConversationNode from graphlink_html_view import HtmlViewNode @@ -103,7 +103,7 @@ def paint(self, painter, option, widget=None): elif self.hover: color = palette.AI_NODE else: - color = QColor("#FFFFFF") + color = QColor(get_surface_color("text_bright")) painter.setPen(QPen(color.darker(120), 1)) painter.setBrush(QBrush(color)) @@ -947,13 +947,13 @@ def drawArrow(self, painter, pos, color): def paint(self, painter, option, widget=None): """Paints the dashed connection line.""" painter.setRenderHint(QPainter.RenderHint.Antialiasing) - pen = QPen(QColor("#888888"), 1.5, Qt.PenStyle.DashLine) + pen = QPen(QColor(get_surface_color("chrome_inactive")), 1.5, Qt.PenStyle.DashLine) painter.setPen(pen) painter.drawPath(self.path) if self.is_animating: for arrow in self.arrows: - self.drawArrow(painter, arrow['pos'], QColor("#888888")) + self.drawArrow(painter, arrow['pos'], QColor(get_surface_color("chrome_inactive"))) def hoverEnterEvent(self, event): """Handles hover enter event.""" @@ -1396,7 +1396,7 @@ def paint(self, painter, option, widget=None): palette = get_current_palette() painter.setRenderHint(QPainter.RenderHint.Antialiasing) - pen_color = QColor("#A2A2A2") # A soft gray-blue + pen_color = QColor(get_surface_color("text_label")) if self.hover: pen_color = pen_color.lighter(130) @@ -1928,7 +1928,7 @@ def update_path(self): def paint(self, painter, option, widget=None): """Paints the gray dashed line for the summary connection.""" painter.setRenderHint(QPainter.RenderHint.Antialiasing) - pen = QPen(QColor("#888888"), 1.5, Qt.PenStyle.DashLine) + pen = QPen(QColor(get_surface_color("chrome_inactive")), 1.5, Qt.PenStyle.DashLine) pen.setCapStyle(Qt.PenCapStyle.RoundCap) painter.setPen(pen) painter.drawPath(self.path) @@ -1954,9 +1954,9 @@ def drawArrow(self, painter, pos, opacity): painter.translate(point) painter.rotate(-angle) - color = QColor("#888888") + color = QColor(get_surface_color("chrome_inactive")) if self.hover: - color = QColor("#BBBBBB") + color = QColor(get_surface_color("text_secondary")) color.setAlphaF(opacity) painter.setBrush(color) diff --git a/graphlink_app/graphlink_context_menu.py b/graphlink_app/graphlink_context_menu.py index 920e2ef..703c7a1 100644 --- a/graphlink_app/graphlink_context_menu.py +++ b/graphlink_app/graphlink_context_menu.py @@ -15,35 +15,36 @@ from PySide6.QtGui import QColor, QPalette from PySide6.QtWidgets import QApplication, QMenu, QWidget -from graphlink_config import get_current_palette, is_monochrome_theme, is_muted_theme +from graphlink_config import get_current_palette, get_surface_color, is_monochrome_theme, is_muted_theme +from graphlink_styles import FONT_FAMILY def _colors() -> dict[str, str]: """Return the menu colors for the active Graphlink theme.""" if is_monochrome_theme(): return { - "surface": "#2A2A2A", - "text": "#DDDDDD", - "border": "#444444", + "surface": get_surface_color("field"), + "text": get_surface_color("text_primary"), + "border": get_surface_color("border"), "hover": "#666666", "disabled": "#777777", } if is_muted_theme(): return { - "surface": "#232323", - "text": "#D1D1D1", - "border": "#383838", + "surface": get_surface_color("field"), + "text": get_surface_color("text_primary"), + "border": get_surface_color("border"), "hover": "#707070", "disabled": "#707070", } return { - "surface": "#272727", - "text": "#DCDCDC", - "border": "#424242", + "surface": get_surface_color("field"), + "text": get_surface_color("text_primary"), + "border": get_surface_color("divider"), "hover": get_current_palette().SELECTION.name(), - "disabled": "#767676", + "disabled": get_surface_color("text_muted"), } @@ -57,7 +58,7 @@ def context_menu_stylesheet() -> str: border: 1px solid {colors['border']}; border-radius: 8px; padding: 4px; - font-family: 'Segoe UI', sans-serif; + font-family: {FONT_FAMILY}; font-size: 12px; }} QMenu::item {{ diff --git a/graphlink_app/graphlink_conversation_node.py b/graphlink_app/graphlink_conversation_node.py index b0aa523..67d3765 100644 --- a/graphlink_app/graphlink_conversation_node.py +++ b/graphlink_app/graphlink_conversation_node.py @@ -7,7 +7,8 @@ from PySide6.QtGui import QPainter, QColor, QBrush, QPen, QPainterPath, QTextDocument, QAction, QCursor, QFont import qtawesome as qta import markdown -from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_neutral_button_colors, get_semantic_color +from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_neutral_button_colors, get_semantic_color, get_surface_color +from graphlink_styles import FONT_FAMILY from graphlink_canvas_items import HoverAnimationMixin from graphlink_lod import draw_lod_card, preview_text, sync_proxy_render_state from graphlink_plugins.graphlink_plugin_context_menu import PluginNodeContextMenu @@ -29,12 +30,12 @@ def __init__(self, text, is_user, timestamp=None, parent=None): self.document = QTextDocument() palette = get_current_palette() - self.document.setDefaultStyleSheet(""" - p, ul, ol, li, blockquote { color: #e0e0e0; margin: 0; font-family: 'Segoe UI'; font-size: 12px; } - pre { background-color: #1e1e1e; padding: 8px; border-radius: 4px; white-space: pre-wrap; font-family: Consolas, monospace; } - a { color: %s; } - .timestamp { color: #666666; font-size: 9px; } - """ % palette.AI_NODE.name()) + self.document.setDefaultStyleSheet(f""" + p, ul, ol, li, blockquote {{ color: {get_surface_color("text_primary")}; margin: 0; font-family: {FONT_FAMILY}; font-size: 12px; }} + pre {{ background-color: {get_surface_color("window")}; padding: 8px; border-radius: 4px; white-space: pre-wrap; font-family: Consolas, monospace; }} + a {{ color: {palette.AI_NODE.name()}; }} + .timestamp {{ color: {get_surface_color("handle_hover")}; font-size: 9px; }} + """) html_content = markdown.markdown(text, extensions=['fenced_code', 'tables']) # Append timestamp to the HTML @@ -138,7 +139,7 @@ def paint(self, painter, option, widget=None): painter.setRenderHint(QPainter.RenderHint.Antialiasing) path = QPainterPath() path.addRoundedRect(self.boundingRect(), 15, 15) - painter.setBrush(QColor("#323232")) + painter.setBrush(get_semantic_color("conversation_ai_bubble")) painter.setPen(Qt.PenStyle.NoPen) painter.drawPath(path) @@ -182,9 +183,9 @@ def __init__(self, parent_node, parent=None): self.widget = QWidget() self.widget.setObjectName("conversationMainWidget") self.widget.setFixedSize(self.NODE_WIDTH, self.NODE_HEIGHT) - self.widget.setStyleSheet(""" - QWidget#conversationMainWidget { background-color: transparent; color: #e0e0e0; } - QWidget#conversationMainWidget QLabel { background-color: transparent; } + self.widget.setStyleSheet(f""" + QWidget#conversationMainWidget {{ background-color: transparent; color: {get_surface_color("text_primary")}; }} + QWidget#conversationMainWidget QLabel {{ background-color: transparent; }} """) self._message_items = [] @@ -305,7 +306,7 @@ def _setup_ui(self): self.internal_view.setRenderHint(QPainter.RenderHint.Antialiasing) self.internal_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.internal_view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) - self.internal_view.setStyleSheet("background-color: #1a1a1a; border: 1px solid #333; border-radius: 6px;") + self.internal_view.setStyleSheet(f"background-color: {get_surface_color('window')}; border: 1px solid {get_surface_color('border')}; border-radius: 6px;") main_layout.addWidget(self.internal_view) input_layout = QHBoxLayout() @@ -340,7 +341,7 @@ def _update_button_style(self): hover = self._blend_color(button_colors["hover"], accent, 0.30) pressed = self._blend_color(button_colors["pressed"], accent, 0.24) border = self._blend_color(button_colors["border"], accent, 0.42) - icon_color = button_colors["muted_icon"] if self._cancel_pending else QColor("#ffffff") + icon_color = button_colors["muted_icon"] if self._cancel_pending else QColor(get_surface_color("text_bright")) icon_name = 'fa5s.stop' tooltip = "Cancelling..." if self._cancel_pending else "Cancel response" else: @@ -516,13 +517,13 @@ def paint(self, painter, option, widget=None): path = QPainterPath() path.addRoundedRect(0, 0, self.width, self.height, 10, 10) - painter.setBrush(QColor("#2d2d2d")) - + painter.setBrush(QColor(get_surface_color("field"))) + node_color = node_colors["border"] pen = QPen(node_color, 1.5) if self.isSelected(): pen = QPen(palette.SELECTION, 2) - elif self.hovered: pen = QPen(QColor("#ffffff"), 2) + elif self.hovered: pen = QPen(QColor(get_surface_color("text_bright")), 2) painter.setPen(pen) painter.drawPath(path) @@ -561,7 +562,7 @@ def paint(self, painter, option, widget=None): return if self.is_collapsed: - painter.setPen(QColor("#ffffff")) + painter.setPen(QColor(get_surface_color("text_bright"))) font = canvas_font(self.scene(), weight=QFont.Weight.Bold) painter.setFont(font) painter.drawText(QRectF(40, 0, self.width - 80, self.height), Qt.AlignmentFlag.AlignVCenter, "Conversation") @@ -570,7 +571,7 @@ def paint(self, painter, option, widget=None): icon.paint(painter, QRect(10, 10, 20, 20)) self.collapse_button_rect = QRectF(self.width - 35, 5, 30, 30) - expand_icon = qta.icon('fa5s.expand-arrows-alt', color='#ffffff' if self.hovered else '#888888') + expand_icon = qta.icon('fa5s.expand-arrows-alt', color=get_surface_color("text_bright") if self.hovered else get_surface_color("chrome_inactive")) expand_icon.paint(painter, QRect(int(self.width - 30), 10, 20, 20)) else: if self.hovered: @@ -579,7 +580,7 @@ def paint(self, painter, option, widget=None): painter.setPen(QColor(255, 255, 255, 150)) painter.drawRoundedRect(self.collapse_button_rect.adjusted(6,6,-6,-6), 4, 4) - icon_pen = QPen(QColor("#ffffff"), 2) + icon_pen = QPen(QColor(get_surface_color("text_bright")), 2) painter.setPen(icon_pen) center = self.collapse_button_rect.center() painter.drawLine(int(center.x() - 4), int(center.y()), int(center.x() + 4), int(center.y())) diff --git a/graphlink_app/graphlink_html_view.py b/graphlink_app/graphlink_html_view.py index 1bd7a7c..4e39b55 100644 --- a/graphlink_app/graphlink_html_view.py +++ b/graphlink_app/graphlink_html_view.py @@ -6,7 +6,7 @@ from PySide6.QtCore import QRectF, Qt, Signal, QPoint, QRect from PySide6.QtGui import QPainter, QColor, QBrush, QPen, QPainterPath, QCursor, QFont import qtawesome as qta -from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_neutral_button_colors, get_semantic_color +from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_neutral_button_colors, get_semantic_color, get_surface_color from graphlink_canvas_items import HoverAnimationMixin from graphlink_lod import draw_lod_card, preview_text, sync_proxy_render_state from graphlink_plugins.graphlink_plugin_context_menu import PluginNodeContextMenu @@ -45,27 +45,27 @@ def __init__(self, parent_node, parent=None): layout.addWidget(self.web_view) def _inject_scrollbar_style(self): - css = """ - ::-webkit-scrollbar { + css = f""" + ::-webkit-scrollbar {{ width: 10px; height: 10px; - background-color: #252525; - } - ::-webkit-scrollbar-track { - background-color: #252525; + background-color: {get_surface_color("node_body")}; + }} + ::-webkit-scrollbar-track {{ + background-color: {get_surface_color("node_body")}; border-radius: 5px; - } - ::-webkit-scrollbar-thumb { - background-color: #555555; + }} + ::-webkit-scrollbar-thumb {{ + background-color: {get_surface_color("handle")}; border-radius: 5px; - border: 1px solid #252525; - } - ::-webkit-scrollbar-thumb:hover { - background-color: #6A6A6A; - } - ::-webkit-scrollbar-corner { + border: 1px solid {get_surface_color("node_body")}; + }} + ::-webkit-scrollbar-thumb:hover {{ + background-color: {get_surface_color("handle_hover")}; + }} + ::-webkit-scrollbar-corner {{ background: transparent; - } + }} """ js = f""" (function() {{ @@ -148,9 +148,9 @@ def __init__(self, parent_node, parent=None): self.widget = QWidget() self.widget.setObjectName("htmlViewMainWidget") self.widget.setFixedSize(self.NODE_WIDTH, self.NODE_HEIGHT) - self.widget.setStyleSheet(""" - QWidget#htmlViewMainWidget { background-color: transparent; color: #E0E0E0; } - QWidget#htmlViewMainWidget QLabel { background-color: transparent; } + self.widget.setStyleSheet(f""" + QWidget#htmlViewMainWidget {{ background-color: transparent; color: {get_surface_color("text_primary")}; }} + QWidget#htmlViewMainWidget QLabel {{ background-color: transparent; }} """) self._setup_ui() @@ -209,15 +209,15 @@ def _setup_ui(self): self.splitter = QSplitter(Qt.Orientation.Vertical) self.splitter.splitterMoved.connect(self._on_splitter_moved) - self.splitter.setStyleSheet(""" - QSplitter::handle:vertical { + self.splitter.setStyleSheet(f""" + QSplitter::handle:vertical {{ height: 8px; - background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3F3F3F, stop:0.5 #555555, stop:1 #3F3F3F); - } - QSplitter::handle:vertical:hover { - background: %s; - } - """ % get_semantic_color("status_success").name()) + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {get_surface_color("border")}, stop:0.5 {get_surface_color("handle")}, stop:1 {get_surface_color("border")}); + }} + QSplitter::handle:vertical:hover {{ + background: {get_semantic_color("status_success").name()}; + }} + """) # --- HTML Input Pane --- input_container = QWidget() @@ -246,13 +246,13 @@ def _setup_ui(self): preview_header_layout.addStretch() self.popout_button = QPushButton() - self.popout_button.setIcon(qta.icon('fa5s.external-link-alt', color='#ccc')) + self.popout_button.setIcon(qta.icon('fa5s.external-link-alt', color=get_surface_color("text_soft"))) self.popout_button.setFixedSize(28, 28) self.popout_button.setToolTip("Open Preview in a New Window") self.popout_button.clicked.connect(self._handle_popout) - self.popout_button.setStyleSheet(""" - QPushButton { border: 1px solid #555; border-radius: 4px; } - QPushButton:hover { background-color: #4F4F4F; } + self.popout_button.setStyleSheet(f""" + QPushButton {{ border: 1px solid {get_surface_color("handle")}; border-radius: 4px; }} + QPushButton:hover {{ background-color: {get_surface_color("border_strong")}; }} """) preview_header_layout.addWidget(self.popout_button) @@ -267,7 +267,7 @@ def _setup_ui(self): self.web_view = QWebEngineView() _harden_preview_web_view(self.web_view) self._inject_scrollbar_style() - self.web_view.setStyleSheet("background-color: #FFFFFF; border-radius: 4px;") + self.web_view.setStyleSheet("background-color: white; border-radius: 4px;") webview_layout.addWidget(self.web_view) else: self.popout_button.setEnabled(False) @@ -278,8 +278,8 @@ def _setup_ui(self): ) error_label.setAlignment(Qt.AlignmentFlag.AlignCenter) error_label.setStyleSheet( - "background-color: #252525; border: 1px dashed #555;" - "color: #888; border-radius: 4px; padding: 20px;" + f"background-color: {get_surface_color('node_body')}; border: 1px dashed {get_surface_color('handle')};" + f"color: {get_surface_color('chrome_inactive')}; border-radius: 4px; padding: 20px;" ) webview_layout.addWidget(error_label) self.render_button.setEnabled(False) @@ -294,12 +294,12 @@ def _setup_ui(self): self.splitter_state = self.splitter.sizes() for widget in [self.html_input]: - widget.setStyleSheet(""" - QTextEdit { - background-color: #252525; border: 1px solid #3F3F3F; - color: #CCCCCC; border-radius: 4px; padding: 5px; + widget.setStyleSheet(f""" + QTextEdit {{ + background-color: {get_surface_color("node_body")}; border: 1px solid {get_surface_color("border")}; + color: {get_surface_color("text_soft")}; border-radius: 4px; padding: 5px; font-family: Consolas, Monaco, monospace; - } + }} """) button_colors = get_neutral_button_colors() @@ -323,34 +323,34 @@ def _setup_ui(self): border-color: {button_colors["border"].darker(105).name()}; }} QPushButton:disabled {{ - background-color: #2B2B2B; - border-color: #353535; - color: #7B7B7B; + background-color: {get_surface_color("field")}; + border-color: {get_surface_color("border")}; + color: {get_surface_color("text_muted")}; }} """) def _inject_scrollbar_style(self): - css = """ - ::-webkit-scrollbar { + css = f""" + ::-webkit-scrollbar {{ width: 10px; height: 10px; - background-color: #252525; - } - ::-webkit-scrollbar-track { - background-color: #252525; + background-color: {get_surface_color("node_body")}; + }} + ::-webkit-scrollbar-track {{ + background-color: {get_surface_color("node_body")}; border-radius: 5px; - } - ::-webkit-scrollbar-thumb { - background-color: #555555; + }} + ::-webkit-scrollbar-thumb {{ + background-color: {get_surface_color("handle")}; border-radius: 5px; - border: 1px solid #252525; - } - ::-webkit-scrollbar-thumb:hover { - background-color: #6A6A6A; - } - ::-webkit-scrollbar-corner { + border: 1px solid {get_surface_color("node_body")}; + }} + ::-webkit-scrollbar-thumb:hover {{ + background-color: {get_surface_color("handle_hover")}; + }} + ::-webkit-scrollbar-corner {{ background: transparent; - } + }} """ js = f""" (function() {{ @@ -445,16 +445,16 @@ def paint(self, painter, option, widget=None): path = QPainterPath() path.addRoundedRect(0, 0, self.width, self.height, 10, 10) - painter.setBrush(QColor("#2D2D2D")) - + painter.setBrush(QColor(get_surface_color("field"))) + node_color = node_colors["border"] pen = QPen(node_color, 1.5) if self.isSelected(): pen = QPen(palette.SELECTION, 2) elif self.hovered: - pen = QPen(QColor("#FFFFFF"), 2) - + pen = QPen(QColor(get_surface_color("text_bright")), 2) + painter.setPen(pen) painter.drawPath(path) @@ -490,7 +490,7 @@ def paint(self, painter, option, widget=None): return if self.is_collapsed: - painter.setPen(QColor("#FFFFFF")) + painter.setPen(QColor(get_surface_color("text_bright"))) font = canvas_font(self.scene(), weight=QFont.Weight.Bold) painter.setFont(font) painter.drawText(QRectF(40, 0, self.width - 80, self.height), Qt.AlignmentFlag.AlignVCenter, "HTML Renderer") @@ -499,7 +499,7 @@ def paint(self, painter, option, widget=None): icon.paint(painter, QRect(10, 10, 20, 20)) self.collapse_button_rect = QRectF(self.width - 35, 5, 30, 30) - expand_icon = qta.icon('fa5s.expand-arrows-alt', color='#FFFFFF' if self.hovered else '#888888') + expand_icon = qta.icon('fa5s.expand-arrows-alt', color=get_surface_color("text_bright") if self.hovered else get_surface_color("chrome_inactive")) expand_icon.paint(painter, QRect(int(self.width - 30), 10, 20, 20)) else: if self.hovered: @@ -508,7 +508,7 @@ def paint(self, painter, option, widget=None): painter.setPen(QColor(255, 255, 255, 150)) painter.drawRoundedRect(self.collapse_button_rect.adjusted(6,6,-6,-6), 4, 4) - icon_pen = QPen(QColor("#FFFFFF"), 2) + icon_pen = QPen(QColor(get_surface_color("text_bright")), 2) painter.setPen(icon_pen) center = self.collapse_button_rect.center() painter.drawLine(int(center.x() - 4), int(center.y()), int(center.x() + 4), int(center.y())) diff --git a/graphlink_app/graphlink_lod.py b/graphlink_app/graphlink_lod.py index 8e3f186..3fba550 100644 --- a/graphlink_app/graphlink_lod.py +++ b/graphlink_app/graphlink_lod.py @@ -3,6 +3,9 @@ from PySide6.QtCore import QPointF, QRectF, Qt from PySide6.QtGui import QBrush, QColor, QFont, QFontMetrics, QLinearGradient, QPainter, QPainterPath, QPen +from graphlink_config import get_surface_color +from graphlink_styles import FONT_FAMILY_NAME + LOD_FULL_THRESHOLD = 0.72 LOD_SUMMARY_THRESHOLD = 0.3 @@ -291,14 +294,14 @@ def draw_lod_card( panel_path.addRoundedRect(panel_rect, border_radius, border_radius) gradient = QLinearGradient(QPointF(panel_rect.left(), panel_rect.top()), QPointF(panel_rect.left(), panel_rect.bottom())) - gradient.setColorAt(0, QColor("#2B2B2B")) - gradient.setColorAt(1, QColor("#1B1B1B")) + gradient.setColorAt(0, QColor(get_surface_color("field"))) + gradient.setColorAt(1, QColor(get_surface_color("window"))) painter.setBrush(QBrush(gradient)) border_color = accent.lighter(110) border_width = 1.35 if hovered: - border_color = QColor("#FFFFFF") + border_color = QColor(get_surface_color("text_bright")) border_width = 1.9 if selected: border_color = selection_color @@ -346,7 +349,7 @@ def draw_lod_card( chip_block_height = 0.0 if badge: badge_font = _fit_font_to_height( - _scaled_font("Segoe UI", 7, scale=min(detail_scale, 1.42), weight=QFont.Weight.DemiBold, max_scale=1.42), + _scaled_font(FONT_FAMILY_NAME, 7, scale=min(detail_scale, 1.42), weight=QFont.Weight.DemiBold, max_scale=1.42), _lod_font_height_limit( panel_rect.height(), zoom, @@ -365,11 +368,11 @@ def draw_lod_card( painter.setBrush(QColor(accent.red(), accent.green(), accent.blue(), 64)) painter.setPen(QPen(QColor(accent.red(), accent.green(), accent.blue(), 116), 1.0)) painter.drawRoundedRect(chip_rect, chip_height / 2, chip_height / 2) - painter.setPen(QColor("#FAFAFA")) + painter.setPen(QColor(get_surface_color("text_bright"))) painter.drawText(chip_rect, Qt.AlignmentFlag.AlignCenter, badge) footer_font = _fit_font_to_height( - _scaled_font("Segoe UI", 9, scale=min(detail_scale, 2.05), weight=QFont.Weight.DemiBold, max_scale=2.05), + _scaled_font(FONT_FAMILY_NAME, 9, scale=min(detail_scale, 2.05), weight=QFont.Weight.DemiBold, max_scale=2.05), _lod_font_height_limit( panel_rect.height(), zoom, @@ -417,7 +420,7 @@ def draw_lod_card( ) glyph_font = _fit_font_to_height( - _scaled_font("Segoe UI", 16, scale=detail_scale, weight=QFont.Weight.DemiBold, max_scale=2.8), + _scaled_font(FONT_FAMILY_NAME, 16, scale=detail_scale, weight=QFont.Weight.DemiBold, max_scale=2.8), _lod_font_height_limit( orb_rect.height(), zoom, @@ -428,7 +431,7 @@ def draw_lod_card( min_point_size=8.0, ) painter.setFont(glyph_font) - painter.setPen(QColor("#F7F7F7")) + painter.setPen(QColor(get_surface_color("text_bright"))) painter.drawText(orb_rect, Qt.AlignmentFlag.AlignCenter, initials_for_title(title)) painter.setBrush(QColor(255, 255, 255, 18)) @@ -439,7 +442,7 @@ def draw_lod_card( footer_rect.adjusted(10.0, 0.0, -10.0, 0.0), title, font=footer_font, - color=QColor("#F6F6F6"), + color=QColor(get_surface_color("text_bright")), max_lines=1, alignment=Qt.AlignmentFlag.AlignHCenter, ) @@ -467,7 +470,7 @@ def draw_lod_card( meta_text_x = content_left if badge: badge_font = _fit_font_to_height( - _scaled_font("Segoe UI", 7, scale=min(detail_scale, 1.5), weight=QFont.Weight.DemiBold, max_scale=1.5), + _scaled_font(FONT_FAMILY_NAME, 7, scale=min(detail_scale, 1.5), weight=QFont.Weight.DemiBold, max_scale=1.5), _lod_font_height_limit( panel_rect.height(), zoom, @@ -490,13 +493,13 @@ def draw_lod_card( painter.setBrush(QColor(accent.red(), accent.green(), accent.blue(), 74)) painter.setPen(QPen(QColor(accent.red(), accent.green(), accent.blue(), 116), 1.0)) painter.drawRoundedRect(badge_rect, badge_height / 2, badge_height / 2) - painter.setPen(QColor("#F7F7F7")) + painter.setPen(QColor(get_surface_color("text_bright"))) painter.drawText(badge_rect, Qt.AlignmentFlag.AlignCenter, badge) meta_text_x = badge_rect.right() + 10.0 if subtitle: subtitle_font = _fit_font_to_height( - _scaled_font("Segoe UI", 8, scale=min(detail_scale, 1.52), weight=QFont.Weight.Medium, max_scale=1.52), + _scaled_font(FONT_FAMILY_NAME, 8, scale=min(detail_scale, 1.52), weight=QFont.Weight.Medium, max_scale=1.52), _lod_font_height_limit( panel_rect.height(), zoom, @@ -511,7 +514,7 @@ def draw_lod_card( QRectF(meta_text_x, header_rect.top(), max(18.0, content_right - meta_text_x), header_rect.height()), subtitle, font=subtitle_font, - color=QColor("#B6B6B6"), + color=QColor(get_surface_color("text_secondary")), max_lines=1, ) @@ -521,7 +524,7 @@ def draw_lod_card( title_font = _fit_font_to_height( _scaled_font( - "Segoe UI", + FONT_FAMILY_NAME, 12 if compact_summary else 13, scale=min(detail_scale, 2.45 if compact_summary else 2.1), weight=QFont.Weight.DemiBold, @@ -545,7 +548,7 @@ def draw_lod_card( title_rect, title, font=title_font, - color=QColor("#FAFAFA"), + color=QColor(get_surface_color("text_bright")), max_lines=title_line_limit, line_gap=2.0, ) @@ -560,7 +563,7 @@ def draw_lod_card( preview_font = _fit_font_to_height( _scaled_font( - "Segoe UI", + FONT_FAMILY_NAME, 9 if compact_summary else 10, scale=min(detail_scale, 1.9 if compact_summary else 1.7), max_scale=1.9 if compact_summary else 1.7, @@ -586,7 +589,7 @@ def draw_lod_card( preview_inner_rect, preview or " ", font=preview_font, - color=QColor("#DCDCDC"), + color=QColor(get_surface_color("text_primary")), max_lines=preview_line_limit, line_gap=2.0, ) @@ -622,7 +625,7 @@ def draw_lod_card( painter.drawPath(panel_path) if search_match: - search_pen = QPen(QColor("#C6C6C6"), 2.2) + search_pen = QPen(QColor(get_surface_color("text_secondary")), 2.2) search_pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin) painter.setPen(search_pen) painter.setBrush(Qt.BrushStyle.NoBrush) diff --git a/graphlink_app/graphlink_nodes/graphlink_node_chat.py b/graphlink_app/graphlink_nodes/graphlink_node_chat.py index 174da48..4007b2a 100644 --- a/graphlink_app/graphlink_nodes/graphlink_node_chat.py +++ b/graphlink_app/graphlink_nodes/graphlink_node_chat.py @@ -15,8 +15,9 @@ from PySide6.QtWidgets import QGraphicsItem from graphlink_canvas_items import Container, Frame, HoverAnimationMixin -from graphlink_config import get_current_palette, get_semantic_color, is_monochrome_theme +from graphlink_config import get_current_palette, get_graph_node_colors, get_semantic_color, get_surface_color, is_monochrome_theme from graphlink_lod import draw_lod_card, lod_mode_for_item, preview_text +from graphlink_styles import FONT_FAMILY_NAME from graphlink_widgets import ScrollBar @@ -143,15 +144,15 @@ def _surface_colors(self): accent = self._role_accent() monochrome = is_monochrome_theme() - body_start = self._mix_color(QColor("#282828"), accent, 0.04 if not monochrome else 0.02) - body_end = self._mix_color(QColor("#1A1A1A"), accent, 0.02 if not monochrome else 0.01) - header_start = self._mix_color(QColor("#323232"), accent, 0.30 if not monochrome else 0.08) - header_end = self._mix_color(QColor("#1C1C1C"), accent, 0.18 if not monochrome else 0.04) - badge_fill = self._mix_color(QColor("#2B2B2B"), accent, 0.58 if not monochrome else 0.12) - badge_text = QColor("#FCFCFC") if self.is_user else QColor("#F6F6F6") - descriptor_text = self._mix_color(QColor("#A5A5A5"), accent, 0.14 if not monochrome else 0.04) - content_panel_fill = QColor("#141414") - content_panel_border = self._mix_color(QColor("#393939"), accent, 0.08 if not monochrome else 0.03) + body_start = self._mix_color(QColor(get_surface_color("field")), accent, 0.04 if not monochrome else 0.02) + body_end = self._mix_color(QColor(get_surface_color("window")), accent, 0.02 if not monochrome else 0.01) + header_start = self._mix_color(get_graph_node_colors()["header_end"], accent, 0.30 if not monochrome else 0.08) + header_end = self._mix_color(QColor(get_surface_color("window")), accent, 0.18 if not monochrome else 0.04) + badge_fill = self._mix_color(QColor(get_surface_color("field")), accent, 0.58 if not monochrome else 0.12) + badge_text = QColor(get_surface_color("text_bright")) + descriptor_text = self._mix_color(QColor(get_surface_color("text_label")), accent, 0.14 if not monochrome else 0.04) + content_panel_fill = QColor(get_surface_color("window")) + content_panel_border = self._mix_color(QColor(get_surface_color("border")), accent, 0.08 if not monochrome else 0.03) return { "accent": accent, @@ -169,11 +170,11 @@ def _surface_colors(self): def _get_default_stylesheet(self, color, font_family, font_size): base_text = QColor(color) accent = self._role_accent() - muted_text = self._mix_color(base_text, QColor("#CFCFCF"), 0.18 if not is_monochrome_theme() else 0.04) + muted_text = self._mix_color(base_text, QColor(get_surface_color("text_soft")), 0.18 if not is_monochrome_theme() else 0.04) link_color = accent.lighter(125) - quote_border = self._mix_color(QColor("#545454"), accent, 0.55 if not is_monochrome_theme() else 0.10) - code_bg = self._mix_color(QColor("#141414"), accent, 0.10 if not is_monochrome_theme() else 0.03) - table_border = self._mix_color(QColor("#404040"), accent, 0.26 if not is_monochrome_theme() else 0.06) + quote_border = self._mix_color(QColor(get_surface_color("border_strong")), accent, 0.55 if not is_monochrome_theme() else 0.10) + code_bg = self._mix_color(QColor(get_surface_color("window")), accent, 0.10 if not is_monochrome_theme() else 0.03) + table_border = self._mix_color(QColor(get_surface_color("border")), accent, 0.26 if not is_monochrome_theme() else 0.06) return f""" body {{ @@ -199,7 +200,7 @@ def _get_default_stylesheet(self, color, font_family, font_size): margin-bottom: 0.2em; }} h1, h2, h3, h4, h5, h6 {{ - color: #FFFFFF; + color: {get_surface_color("text_bright")}; font-family: '{font_family}'; font-weight: bold; margin-top: 0.2em; @@ -260,9 +261,9 @@ def _get_default_stylesheet(self, color, font_family, font_size): """ def _setup_document(self): - font_family = "Segoe UI" + font_family = FONT_FAMILY_NAME font_size = 10 - color = "#DDDDDD" + color = get_surface_color("text_primary") if self.scene(): font_family = self.scene().font_family @@ -394,7 +395,7 @@ def _paint_header(self, painter, current_width): painter.setPen(QPen(colors["content_panel_border"], 1)) painter.drawLine(10, self.HEADER_HEIGHT, current_width - 10, self.HEADER_HEIGHT) - font_family = self.scene().font_family if self.scene() else "Segoe UI" + font_family = self.scene().font_family if self.scene() else FONT_FAMILY_NAME badge_font = QFont(font_family, 8, QFont.Weight.DemiBold) painter.setFont(badge_font) badge_metrics = QFontMetrics(badge_font) @@ -424,7 +425,7 @@ def _paint_collapse_button(self, painter, button_x): painter.setPen(QColor(255, 255, 255, 110)) painter.drawRoundedRect(self.collapse_button_rect, 4, 4) - icon_pen = QPen(QColor("#FFFFFF"), 1.8) + icon_pen = QPen(QColor(get_surface_color("text_bright")), 1.8) painter.setPen(icon_pen) center = self.collapse_button_rect.center() painter.drawLine(int(center.x() - 4), int(center.y()), int(center.x() + 4), int(center.y())) @@ -483,7 +484,7 @@ def paint(self, painter, option, widget=None): if self.isSelected() and not is_dragging: pen = QPen(palette.SELECTION, 2.2) elif self.hovered: - pen = QPen(QColor("#FFFFFF"), 2) + pen = QPen(QColor(get_surface_color("text_bright")), 2) painter.setPen(Qt.PenStyle.NoPen) painter.drawPath(path) @@ -529,14 +530,14 @@ def paint(self, painter, option, widget=None): docked_child_count = self.docked_child_count() if docked_child_count: - indicator_color = QColor("#A2A2A2").lighter(130) + indicator_color = QColor(get_surface_color("text_label")).lighter(130) painter.setBrush(indicator_color) painter.setPen(Qt.PenStyle.NoPen) badge_width = 26 if docked_child_count < 10 else 32 badge_rect = QRectF(current_width - badge_width - 38, 8, badge_width, 18) painter.drawRoundedRect(badge_rect, 9, 9) - painter.setPen(QColor("#141414")) - count_font = QFont(self.scene().font_family if self.scene() else "Segoe UI", 8, QFont.Weight.Bold) + painter.setPen(QColor(get_surface_color("window"))) + count_font = QFont(self.scene().font_family if self.scene() else FONT_FAMILY_NAME, 8, QFont.Weight.Bold) painter.setFont(count_font) painter.drawText(badge_rect, Qt.AlignmentFlag.AlignCenter, str(docked_child_count)) @@ -569,10 +570,10 @@ def paint(self, painter, option, widget=None): painter.drawPath(path) if self.is_collapsed: - font_family = self.scene().font_family if self.scene() else "Segoe UI" + font_family = self.scene().font_family if self.scene() else FONT_FAMILY_NAME snippet_font = QFont(font_family, 9) painter.setFont(snippet_font) - painter.setPen(QColor("#F3F3F3")) + painter.setPen(QColor(get_surface_color("text_strong"))) metrics = QFontMetrics(snippet_font) text_to_show = self.text.split('\n')[0].strip() or "[Empty]" elided_text = metrics.elidedText(text_to_show, Qt.TextElideMode.ElideRight, current_width - 26) diff --git a/graphlink_app/graphlink_nodes/graphlink_node_code.py b/graphlink_app/graphlink_nodes/graphlink_node_code.py index 441c0f5..e9369ff 100644 --- a/graphlink_app/graphlink_nodes/graphlink_node_code.py +++ b/graphlink_app/graphlink_nodes/graphlink_node_code.py @@ -4,8 +4,9 @@ from PySide6.QtWidgets import QApplication, QGraphicsItem from graphlink_canvas_items import Container, HoverAnimationMixin -from graphlink_config import canvas_font, canvas_font_color, get_current_palette, get_graph_node_colors, get_semantic_color +from graphlink_config import canvas_font, canvas_font_color, get_current_palette, get_graph_node_colors, get_semantic_color, get_surface_color from graphlink_lod import draw_lod_card, lod_mode_for_item, preview_text +from graphlink_styles import FONT_FAMILY_NAME from graphlink_widgets import ScrollBar try: @@ -28,7 +29,7 @@ def __init__(self, style='monokai'): def highlight(self, code, language): if not PYGMENTS_AVAILABLE: - return f'
{code}'
+ return f'{code}'
try:
if not language:
lexer = guess_lexer(code)
@@ -83,9 +84,9 @@ def __init__(self, code, language, parent_content_node, parent=None):
def _set_document_style(self):
scene = self.scene()
- family = getattr(scene, "font_family", "Segoe UI")
+ family = getattr(scene, "font_family", FONT_FAMILY_NAME)
size = max(1, int(getattr(scene, "font_size", 10)))
- color = canvas_font_color(scene, "#dddddd").name()
+ color = canvas_font_color(scene, get_surface_color("text_primary")).name()
stylesheet = self.highlighter.get_stylesheet()
stylesheet += f" body, pre {{ font-family: '{family}'; font-size: {size}pt; color: {color}; }}"
self.document.setDefaultStyleSheet(stylesheet)
@@ -143,7 +144,7 @@ def paint(self, painter, option, widget=None):
path = QPainterPath()
path.addRoundedRect(0, 0, self.width, self.height, 10, 10)
- painter.setBrush(QColor("#1e1e1e"))
+ painter.setBrush(QColor(get_surface_color("window")))
is_dragging = self.scene() and getattr(self.scene(), 'is_rubber_band_dragging', False)
@@ -178,7 +179,7 @@ def paint(self, painter, option, widget=None):
painter.setBrush(node_colors["header_start"])
painter.drawPath(header_path)
- painter.setPen(QColor("#cccccc"))
+ painter.setPen(QColor(get_surface_color("text_soft")))
font = canvas_font(self.scene(), delta=-1)
painter.setFont(font)
metrics = QFontMetrics(font)
@@ -190,7 +191,7 @@ def paint(self, painter, option, widget=None):
)
painter.drawText(label_rect, Qt.AlignmentFlag.AlignVCenter, label_text)
- copy_icon = qta.icon('fa5s.copy', color='#cccccc')
+ copy_icon = qta.icon('fa5s.copy', color=get_surface_color("text_soft"))
copy_icon.paint(painter, QRectF(self.width - 28, 7, 16, 16).toRect())
painter.save()
diff --git a/graphlink_app/graphlink_nodes/graphlink_node_document.py b/graphlink_app/graphlink_nodes/graphlink_node_document.py
index 735f050..1a957cb 100644
--- a/graphlink_app/graphlink_nodes/graphlink_node_document.py
+++ b/graphlink_app/graphlink_nodes/graphlink_node_document.py
@@ -8,8 +8,9 @@
from graphlink_audio import format_duration
from graphlink_canvas_items import Container, HoverAnimationMixin
-from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors
+from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_surface_color
from graphlink_lod import draw_lod_card, lod_mode_for_item, preview_text
+from graphlink_styles import FONT_FAMILY_NAME
from graphlink_widgets import ScrollBar
@@ -178,9 +179,9 @@ def _should_show_audio_preview(self, content, audio_details):
return True
def _setup_document(self):
- font_family = "Segoe UI"
+ font_family = FONT_FAMILY_NAME
font_size = 10
- color = "#DDDDDD"
+ color = get_surface_color("text_primary")
if self.scene():
font_family = self.scene().font_family
@@ -192,13 +193,13 @@ def _setup_document(self):
f"p {{ margin: 0 0 0.45em 0; }}"
"table.meta { width: 100%; border-collapse: collapse; margin: 0 0 12px 0; }"
"table.meta td { padding: 6px 0; vertical-align: top; }"
- "td.meta-label { color: #979797; font-size: 9pt; font-weight: 600; width: 88px; min-width: 88px; max-width: 88px; padding-right: 18px; white-space: nowrap; }"
- "td.meta-value { color: #F1F1F1; font-size: 9.5pt; }"
- "p.attachment-kicker { color: #A0A0A0; font-size: 8.5pt; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; margin: 0 0 4px 0; }"
- "p.attachment-title { color: #F7F7F7; font-size: 12pt; font-weight: 700; margin: 0 0 12px 0; }"
- "p.section-label { color: #979797; font-size: 8.5pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; margin: 6px 0 8px 0; }"
- "div.preview { background: #121212; border: 1px solid #393939; border-radius: 10px; padding: 10px 12px; }"
- "pre.preview-text { margin: 0; color: #DDDDDD; font-family: 'Consolas', 'Courier New', monospace; white-space: pre-wrap; }"
+ f"td.meta-label {{ color: {get_surface_color('text_label')}; font-size: 9pt; font-weight: 600; width: 88px; min-width: 88px; max-width: 88px; padding-right: 18px; white-space: nowrap; }}"
+ f"td.meta-value {{ color: {get_surface_color('text_strong')}; font-size: 9.5pt; }}"
+ f"p.attachment-kicker {{ color: {get_surface_color('text_label')}; font-size: 8.5pt; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; margin: 0 0 4px 0; }}"
+ f"p.attachment-title {{ color: {get_surface_color('text_bright')}; font-size: 12pt; font-weight: 700; margin: 0 0 12px 0; }}"
+ f"p.section-label {{ color: {get_surface_color('text_label')}; font-size: 8.5pt; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; margin: 6px 0 8px 0; }}"
+ f"div.preview {{ background: {get_surface_color('inset_deep')}; border: 1px solid {get_surface_color('border')}; border-radius: 10px; padding: 10px 12px; }}"
+ f"pre.preview-text {{ margin: 0; color: {get_surface_color('text_primary')}; font-family: 'Consolas', 'Courier New', monospace; white-space: pre-wrap; }}"
)
self.document.setDefaultStyleSheet(stylesheet)
self.document.setHtml(self._build_attachment_html())
@@ -376,29 +377,29 @@ def _paint_action_button(self, painter, rect, icon_name, hovered):
painter.setBrush(button_bg)
painter.setPen(button_border)
painter.drawRoundedRect(rect, 4, 4)
- icon = qta.icon(icon_name, color="#FFFFFF" if hovered else "#D7D7D7")
+ icon = qta.icon(icon_name, color=get_surface_color("text_bright") if hovered else get_surface_color("text_primary"))
icon.paint(painter, rect.adjusted(3, 3, -3, -3).toRect())
def _paint_collapsed_state(self, painter, current_width, current_height, pen):
node_colors = get_graph_node_colors()
- painter.setBrush(QColor("#272727"))
+ painter.setBrush(QColor(get_surface_color("field")))
painter.setPen(pen)
painter.drawRoundedRect(0, 0, current_width, current_height, current_height / 2, current_height / 2)
- icon = qta.icon(self._icon_name(), color="#D9D9D9")
+ icon = qta.icon(self._icon_name(), color=get_surface_color("text_primary"))
icon.paint(painter, QRectF(12, 14, 16, 16).toRect())
- title_font = QFont(self.scene().font_family if self.scene() else "Segoe UI", 9, QFont.Weight.Bold)
+ title_font = QFont(self.scene().font_family if self.scene() else FONT_FAMILY_NAME, 9, QFont.Weight.Bold)
painter.setFont(title_font)
- painter.setPen(QColor("#F3F3F3"))
+ painter.setPen(QColor(get_surface_color("text_strong")))
title_metrics = QFontMetrics(title_font)
title_width = current_width - 84
elided_title = title_metrics.elidedText(self.title or self._subtitle_text(), Qt.TextElideMode.ElideRight, title_width)
painter.drawText(QRectF(36, 9, title_width, 16), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, elided_title)
- subtitle_font = QFont(self.scene().font_family if self.scene() else "Segoe UI", 8)
+ subtitle_font = QFont(self.scene().font_family if self.scene() else FONT_FAMILY_NAME, 8)
painter.setFont(subtitle_font)
- painter.setPen(QColor("#B1B1B1"))
+ painter.setPen(QColor(get_surface_color("text_secondary")))
subtitle_metrics = QFontMetrics(subtitle_font)
subtitle_text = subtitle_metrics.elidedText(self.preview_label or self._subtitle_text(), Qt.TextElideMode.ElideRight, title_width)
painter.drawText(QRectF(36, 26, title_width, 14), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, subtitle_text)
@@ -444,7 +445,7 @@ def paint(self, painter, option, widget=None):
else:
path = QPainterPath()
path.addRoundedRect(0, 0, self.width, self.height, 10, 10)
- painter.setBrush(QColor("#2D2D2D"))
+ painter.setBrush(QColor(get_surface_color("field")))
painter.setPen(pen)
painter.drawPath(path)
@@ -454,10 +455,10 @@ def paint(self, painter, option, widget=None):
painter.setBrush(node_colors["header_start"])
painter.drawPath(header_path)
- icon = qta.icon(self._icon_name(), color="#CCCCCC")
+ icon = qta.icon(self._icon_name(), color=get_surface_color("text_soft"))
icon.paint(painter, QRectF(10, 7, 16, 16).toRect())
- painter.setPen(QColor("#CCCCCC"))
+ painter.setPen(QColor(get_surface_color("text_soft")))
title_font = canvas_font(self.scene(), delta=-1, weight=QFont.Weight.Bold)
painter.setFont(title_font)
title_metrics = QFontMetrics(title_font)
@@ -473,12 +474,12 @@ def paint(self, painter, option, widget=None):
painter.setBrush(QColor(255, 255, 255, 18))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawRoundedRect(badge_rect, 8, 8)
- painter.setPen(QColor("#D9D9D9"))
+ painter.setPen(QColor(get_surface_color("text_primary")))
painter.drawText(badge_rect, Qt.AlignmentFlag.AlignCenter, badge_metrics.elidedText(badge_text, Qt.TextElideMode.ElideRight, int(badge_rect.width() - 10)))
panel_rect = self._content_panel_rect()
- painter.setBrush(QColor("#1C1C1C"))
- painter.setPen(QPen(QColor("#404040"), 1))
+ painter.setBrush(QColor(get_surface_color("window")))
+ painter.setPen(QPen(QColor(get_surface_color("border")), 1))
painter.drawRoundedRect(panel_rect, 11, 11)
painter.save()
diff --git a/graphlink_app/graphlink_nodes/graphlink_node_image.py b/graphlink_app/graphlink_nodes/graphlink_node_image.py
index c24428a..300c7cd 100644
--- a/graphlink_app/graphlink_nodes/graphlink_node_image.py
+++ b/graphlink_app/graphlink_nodes/graphlink_node_image.py
@@ -4,7 +4,7 @@
from PySide6.QtWidgets import QGraphicsItem
from graphlink_canvas_items import Container, HoverAnimationMixin
-from graphlink_config import canvas_font, canvas_font_color, get_current_palette, get_graph_node_colors, get_semantic_color
+from graphlink_config import canvas_font, canvas_font_color, get_current_palette, get_graph_node_colors, get_semantic_color, get_surface_color
from graphlink_lod import draw_lod_card, lod_mode_for_item, preview_text
@@ -59,7 +59,7 @@ def paint(self, painter, option, widget=None):
path = QPainterPath()
path.addRoundedRect(0, 0, self.width, self.height, 10, 10)
- painter.setBrush(QColor("#2D2D2D"))
+ painter.setBrush(QColor(get_surface_color("field")))
is_dragging = self.scene() and getattr(self.scene(), 'is_rubber_band_dragging', False)
@@ -94,10 +94,10 @@ def paint(self, painter, option, widget=None):
painter.setBrush(node_colors["header_start"])
painter.drawPath(header_path)
- icon = qta.icon('fa5s.image', color='#CCCCCC')
+ icon = qta.icon('fa5s.image', color=get_surface_color("text_soft"))
icon.paint(painter, QRectF(10, 7, 16, 16).toRect())
- painter.setPen(QColor("#CCCCCC"))
+ painter.setPen(QColor(get_surface_color("text_soft")))
font = canvas_font(self.scene(), delta=-1)
painter.setFont(font)
metrics = QFontMetrics(font)
@@ -126,7 +126,7 @@ def paint(self, painter, option, widget=None):
painter.drawImage(target_rect, self.image)
else:
painter.save()
- placeholder_color = canvas_font_color(self.scene(), "#B1B1B1")
+ placeholder_color = canvas_font_color(self.scene(), get_surface_color("text_secondary"))
placeholder_color.setAlpha(180)
painter.setPen(QPen(placeholder_color, 1, Qt.PenStyle.DashLine))
painter.setBrush(QColor(255, 255, 255, 10))
diff --git a/graphlink_app/graphlink_nodes/graphlink_node_thinking.py b/graphlink_app/graphlink_nodes/graphlink_node_thinking.py
index dcf9569..62862aa 100644
--- a/graphlink_app/graphlink_nodes/graphlink_node_thinking.py
+++ b/graphlink_app/graphlink_nodes/graphlink_node_thinking.py
@@ -5,8 +5,9 @@
from PySide6.QtWidgets import QGraphicsItem
from graphlink_canvas_items import Container, HoverAnimationMixin
-from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors
+from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_surface_color
from graphlink_lod import draw_lod_card, lod_mode_for_item, preview_text
+from graphlink_styles import FONT_FAMILY_NAME
from graphlink_widgets import ScrollBar
@@ -52,9 +53,9 @@ def __init__(self, thinking_text, parent_content_node, parent=None):
self._recalculate_geometry()
def _setup_document(self):
- font_family = "Segoe UI"
+ font_family = FONT_FAMILY_NAME
font_size = 9
- color = "#B0B0B0"
+ color = get_surface_color("text_secondary")
if self.scene():
font_family = self.scene().font_family
@@ -64,10 +65,10 @@ def _setup_document(self):
stylesheet = (
f"body {{ color: {color}; font-family: '{font_family}'; font-size: {font_size}pt; margin: 0; }}"
f"p {{ color: {color}; margin: 0 0 0.6em 0; }}"
- "p.thinking-kicker { color: #9E9E9E; font-size: 8.5pt; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; margin: 0 0 8px 0; }"
- "blockquote { border-left: 3px solid #545454; padding-left: 10px; margin: 0.35em 0 0.8em 0; color: #CCCCCC; }"
- "pre { background: #121212; border: 1px solid #393939; border-radius: 8px; padding: 10px 12px; margin: 0.35em 0 0.8em 0; color: #DCDCDC; }"
- "code { background: #121212; border-radius: 4px; padding: 1px 4px; color: #DCDCDC; }"
+ f"p.thinking-kicker {{ color: {get_surface_color('text_label')}; font-size: 8.5pt; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; margin: 0 0 8px 0; }}"
+ f"blockquote {{ border-left: 3px solid {get_surface_color('border_strong')}; padding-left: 10px; margin: 0.35em 0 0.8em 0; color: {get_surface_color('text_soft')}; }}"
+ f"pre {{ background: {get_surface_color('inset_deep')}; border: 1px solid {get_surface_color('border')}; border-radius: 8px; padding: 10px 12px; margin: 0.35em 0 0.8em 0; color: {get_surface_color('text_primary')}; }}"
+ f"code {{ background: {get_surface_color('inset_deep')}; border-radius: 4px; padding: 1px 4px; color: {get_surface_color('text_primary')}; }}"
"pre code { background: transparent; padding: 0; }"
"ul, ol { margin: 0 0 0.75em 0; padding-left: 20px; }"
"li { margin-bottom: 0.2em; }"
@@ -166,7 +167,7 @@ def paint(self, painter, option, widget=None):
path = QPainterPath()
path.addRoundedRect(0, 0, self.width, self.height, 10, 10)
- painter.setBrush(QColor("#2D2D2D"))
+ painter.setBrush(QColor(get_surface_color("field")))
is_dragging = self.scene() and getattr(self.scene(), 'is_rubber_band_dragging', False)
@@ -204,10 +205,10 @@ def paint(self, painter, option, widget=None):
painter.setBrush(node_colors["header_start"])
painter.drawPath(header_path)
- icon = qta.icon('fa5s.brain', color='#CCCCCC')
+ icon = qta.icon('fa5s.brain', color=get_surface_color("text_soft"))
icon.paint(painter, QRectF(10, 7, 16, 16).toRect())
- painter.setPen(QColor("#CCCCCC"))
+ painter.setPen(QColor(get_surface_color("text_soft")))
font = canvas_font(self.scene(), delta=-1, weight=QFont.Weight.Bold)
painter.setFont(font)
title_metrics = QFontMetrics(font)
@@ -217,17 +218,17 @@ def paint(self, painter, option, widget=None):
title_text = title_metrics.elidedText("Assistant's Thoughts", Qt.TextElideMode.ElideRight, int(title_rect.width()))
painter.drawText(title_rect, Qt.AlignmentFlag.AlignVCenter, title_text)
- button_bg_color = QColor("#555") if self.dock_button_hovered else QColor("#444")
+ button_bg_color = QColor(get_surface_color("handle")) if self.dock_button_hovered else QColor(get_surface_color("divider"))
painter.setBrush(button_bg_color)
painter.setPen(Qt.PenStyle.NoPen)
painter.drawRoundedRect(self.dock_button_rect, 4, 4)
- dock_icon_color = "#FFFFFF" if self.dock_button_hovered else "#AAAAAA"
+ dock_icon_color = get_surface_color("text_bright") if self.dock_button_hovered else get_surface_color("text_label")
dock_icon = qta.icon('fa5s.arrow-up', color=dock_icon_color)
dock_icon.paint(painter, self.dock_button_rect.adjusted(3, 3, -3, -3).toRect())
panel_rect = self._content_panel_rect()
- painter.setBrush(QColor("#1C1C1C"))
- painter.setPen(QPen(QColor("#404040"), 1))
+ painter.setBrush(QColor(get_surface_color("window")))
+ painter.setPen(QPen(QColor(get_surface_color("border")), 1))
painter.drawRoundedRect(panel_rect, 11, 11)
painter.save()
diff --git a/graphlink_app/graphlink_plugins/common/combo.py b/graphlink_app/graphlink_plugins/common/combo.py
index 5352066..4c54212 100644
--- a/graphlink_app/graphlink_plugins/common/combo.py
+++ b/graphlink_app/graphlink_plugins/common/combo.py
@@ -34,6 +34,8 @@
)
from shiboken6 import isValid as _is_qt_object_valid
+from graphlink_config import get_surface_color
+
def _qt_object_is_alive(obj):
"""Return whether a Qt wrapper still owns a live C++ object."""
@@ -92,8 +94,8 @@ def apply_style(self, accent_color="#656565"):
self.setStyleSheet(
f"""
QFrame#pluginComboPopupFrame {{
- background-color: #222222;
- border: 1px solid #3A3A3A;
+ background-color: {get_surface_color("node_body")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 10px;
}}
QFrame#pluginComboPopupShell {{
@@ -102,29 +104,29 @@ def apply_style(self, accent_color="#656565"):
}}
QListWidget#pluginComboPopupList {{
background: transparent;
- color: #FFFFFF;
+ color: {get_surface_color("text_bright")};
border: none;
outline: none;
padding: 2px;
}}
QListWidget#pluginComboPopupList::item {{
background: transparent;
- color: #FFFFFF;
+ color: {get_surface_color("text_bright")};
border: none;
border-radius: 6px;
min-height: 26px;
padding: 6px 10px;
}}
QListWidget#pluginComboPopupList::item:hover {{
- background-color: #2F2F2F;
+ background-color: {get_surface_color("field")};
}}
QListWidget#pluginComboPopupList::item:selected {{
background-color: {accent_color};
- color: #FFFFFF;
+ color: {get_surface_color("text_bright")};
}}
QListWidget#pluginComboPopupList::item:selected:hover {{
background-color: {accent_color};
- color: #FFFFFF;
+ color: {get_surface_color("text_bright")};
}}
"""
)
diff --git a/graphlink_app/graphlink_plugins/graphlink_plugin_artifact.py b/graphlink_app/graphlink_plugins/graphlink_plugin_artifact.py
index 8fc4e17..9ea3e43 100644
--- a/graphlink_app/graphlink_plugins/graphlink_plugin_artifact.py
+++ b/graphlink_app/graphlink_plugins/graphlink_plugin_artifact.py
@@ -10,55 +10,57 @@
from PySide6.QtGui import QPainter, QColor, QPen, QPainterPath, QFont, QBrush, QLinearGradient
import qtawesome as qta
from graphlink_config import canvas_font, get_current_palette
-from graphlink_config import get_semantic_color
+from graphlink_config import get_semantic_color, get_surface_color
+from graphlink_styles import FONT_FAMILY
from graphlink_canvas_items import HoverAnimationMixin
from graphlink_connections import ConnectionItem
from graphlink_lod import draw_lod_card, preview_text, sync_proxy_render_state
from graphlink_plugins.graphlink_plugin_context_menu import PluginNodeContextMenu
-ARTIFACT_SCROLLBAR_STYLE = """
- QScrollBar:vertical {
- background: #252525;
+def artifact_scrollbar_style():
+ return f"""
+ QScrollBar:vertical {{
+ background: {get_surface_color("node_body")};
width: 10px;
margin: 0px;
border-radius: 5px;
- }
- QScrollBar::handle:vertical {
- background-color: #555555;
+ }}
+ QScrollBar::handle:vertical {{
+ background-color: {get_surface_color("handle")};
min-height: 25px;
border-radius: 5px;
- }
- QScrollBar::handle:vertical:hover {
- background-color: #6A6A6A;
- }
- QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
+ }}
+ QScrollBar::handle:vertical:hover {{
+ background-color: {get_surface_color("handle_hover")};
+ }}
+ QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0px;
background: none;
- }
- QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
+ }}
+ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{
background: none;
- }
- QScrollBar:horizontal {
- background: #252525;
+ }}
+ QScrollBar:horizontal {{
+ background: {get_surface_color("node_body")};
height: 10px;
margin: 0px;
border-radius: 5px;
- }
- QScrollBar::handle:horizontal {
- background-color: #555555;
+ }}
+ QScrollBar::handle:horizontal {{
+ background-color: {get_surface_color("handle")};
min-width: 25px;
border-radius: 5px;
- }
- QScrollBar::handle:horizontal:hover {
- background-color: #6A6A6A;
- }
- QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
+ }}
+ QScrollBar::handle:horizontal:hover {{
+ background-color: {get_surface_color("handle_hover")};
+ }}
+ QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
width: 0px;
background: none;
- }
- QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
+ }}
+ QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {{
background: none;
- }
+ }}
"""
@@ -156,9 +158,9 @@ def __init__(self, parent_node, parent=None):
self.widget = QWidget()
self.widget.setObjectName("artifactMainWidget")
self.widget.setFixedSize(self.NODE_WIDTH, self.NODE_HEIGHT)
- self.widget.setStyleSheet("""
- QWidget#artifactMainWidget { background-color: transparent; color: #E0E0E0; font-family: 'Segoe UI', sans-serif; }
- QWidget#artifactMainWidget QLabel { background-color: transparent; }
+ self.widget.setStyleSheet(f"""
+ QWidget#artifactMainWidget {{ background-color: transparent; color: {get_surface_color("text_primary")}; font-family: {FONT_FAMILY}; }}
+ QWidget#artifactMainWidget QLabel {{ background-color: transparent; }}
""")
self._setup_ui()
@@ -255,7 +257,7 @@ def _setup_ui(self):
artifact_color = get_semantic_color("artifact")
artifact_hover_color = artifact_color.lighter(115)
artifact_icon_text = artifact_color.name()
- artifact_button_icon = "#1E1E1E" if artifact_color.lightness() > 150 else "#F3F3F3"
+ artifact_button_icon = get_surface_color("window") if artifact_color.lightness() > 150 else get_surface_color("text_strong")
main_layout = QVBoxLayout(self.widget)
main_layout.setContentsMargins(15, 15, 15, 15)
main_layout.setSpacing(10)
@@ -276,17 +278,17 @@ def _setup_ui(self):
# --- Separator Line ---
line = QFrame()
line.setFrameShape(QFrame.Shape.HLine)
- line.setStyleSheet("background-color: #3F3F3F; border: none; height: 1px;")
+ line.setStyleSheet(f"background-color: {get_surface_color('border')}; border: none; height: 1px;")
main_layout.addWidget(line)
# --- Main Splitter ---
self.splitter = QSplitter(Qt.Orientation.Horizontal)
self.splitter.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
- self.splitter.setStyleSheet("""
- QSplitter::handle:horizontal {
+ self.splitter.setStyleSheet(f"""
+ QSplitter::handle:horizontal {{
width: 8px;
- background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #3F3F3F, stop:0.5 #555555, stop:1 #3F3F3F);
- }
+ background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 {get_surface_color("border")}, stop:0.5 {get_surface_color("handle")}, stop:1 {get_surface_color("border")});
+ }}
""" + f"""
QSplitter::handle:horizontal:hover {{ background: {artifact_icon_text}; }}
""")
@@ -300,18 +302,18 @@ def _setup_ui(self):
# Chat Log
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
- self.chat_display.setStyleSheet("""
- QTextEdit {
- background-color: #1E1E1E;
- border: 1px solid #3F3F3F;
+ self.chat_display.setStyleSheet(f"""
+ QTextEdit {{
+ background-color: {get_surface_color("window")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 6px;
padding: 10px;
- }
- """ + ARTIFACT_SCROLLBAR_STYLE)
- self.chat_display.document().setDefaultStyleSheet("""
- p, ul, ol, li { color: #D4D4D4; font-family: 'Segoe UI', sans-serif; font-size: 13px; line-height: 1.4; margin-top: 2px; margin-bottom: 8px;}
- pre { background-color: #2D2D2D; padding: 8px; border-radius: 4px; font-family: Consolas, monospace; color: #D8D8D8; }
- code { background-color: #3F3F3F; padding: 2px 4px; border-radius: 4px; font-family: Consolas, monospace; }
+ }}
+ """ + artifact_scrollbar_style())
+ self.chat_display.document().setDefaultStyleSheet(f"""
+ p, ul, ol, li {{ color: {get_surface_color("text_soft")}; font-family: {FONT_FAMILY}; font-size: 13px; line-height: 1.4; margin-top: 2px; margin-bottom: 8px;}}
+ pre {{ background-color: {get_surface_color("field")}; padding: 8px; border-radius: 4px; font-family: Consolas, monospace; color: {get_surface_color("text_primary")}; }}
+ code {{ background-color: {get_surface_color("border")}; padding: 2px 4px; border-radius: 4px; font-family: Consolas, monospace; }}
""")
left_layout.addWidget(self.chat_display, stretch=1)
@@ -319,12 +321,12 @@ def _setup_ui(self):
input_container = QWidget()
input_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
input_container.setMinimumHeight(72)
- input_container.setStyleSheet("""
- QWidget {
- background-color: #1E1E1E;
- border: 1px solid #3F3F3F;
+ input_container.setStyleSheet(f"""
+ QWidget {{
+ background-color: {get_surface_color("window")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 8px;
- }
+ }}
""")
input_layout = QHBoxLayout(input_container)
input_layout.setContentsMargins(12, 10, 10, 10)
@@ -338,16 +340,16 @@ def _setup_ui(self):
self.instruction_input.setWordWrapMode(self.instruction_input.wordWrapMode())
self.instruction_input.setMinimumHeight(50)
self.instruction_input.setMaximumHeight(110)
- self.instruction_input.setStyleSheet("""
- QTextEdit {
+ self.instruction_input.setStyleSheet(f"""
+ QTextEdit {{
background-color: transparent;
border: none;
- color: #FFFFFF;
+ color: {get_surface_color("text_bright")};
font-size: 13px;
- font-family: 'Segoe UI', sans-serif;
+ font-family: {FONT_FAMILY};
padding: 2px 0px;
- }
- """ + ARTIFACT_SCROLLBAR_STYLE)
+ }}
+ """ + artifact_scrollbar_style())
self.instruction_input.textChanged.connect(self._on_instruction_changed)
self.instruction_input.submit_requested.connect(self._on_instruction_submit_requested)
input_layout.addWidget(self.instruction_input, stretch=1)
@@ -360,7 +362,7 @@ def _setup_ui(self):
self.update_button.setStyleSheet(f"""
QPushButton {{ background-color: {artifact_icon_text}; border: none; border-radius: 20px; margin: 0px; }}
QPushButton:hover {{ background-color: {artifact_hover_color.name()}; }}
- QPushButton:disabled {{ background-color: #444444; }}
+ QPushButton:disabled {{ background-color: {get_surface_color("divider")}; }}
""")
self.update_button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
input_layout.addWidget(self.update_button)
@@ -375,44 +377,44 @@ def _setup_ui(self):
self.tabs = QTabWidget()
self.tabs.setStyleSheet(f"""
- QTabWidget::pane {{ border: 1px solid #3F3F3F; background: #1E1E1E; border-radius: 6px; border-top-left-radius: 0px; }}
- QTabBar::tab {{ background: transparent; color: #888888; padding: 6px 16px; border: none; font-weight: bold; font-size: 12px; margin-right: 4px; }}
+ QTabWidget::pane {{ border: 1px solid {get_surface_color("border")}; background: {get_surface_color("window")}; border-radius: 6px; border-top-left-radius: 0px; }}
+ QTabBar::tab {{ background: transparent; color: {get_surface_color("chrome_inactive")}; padding: 6px 16px; border: none; font-weight: bold; font-size: 12px; margin-right: 4px; }}
QTabBar::tab:selected {{ color: {artifact_icon_text}; border-bottom: 2px solid {artifact_icon_text}; }}
- QTabBar::tab:hover:!selected {{ color: #FFFFFF; }}
+ QTabBar::tab:hover:!selected {{ color: {get_surface_color("text_bright")}; }}
""")
self.raw_editor = QPlainTextEdit()
self.raw_editor.setPlaceholderText("The raw markdown / code will appear here. You can manually edit it.")
- self.raw_editor.setStyleSheet("""
- QPlainTextEdit {
- background-color: #1E1E1E;
- color: #D8D8D8;
+ self.raw_editor.setStyleSheet(f"""
+ QPlainTextEdit {{
+ background-color: {get_surface_color("window")};
+ color: {get_surface_color("text_primary")};
font-family: Consolas, Monaco, monospace;
border: none;
padding: 12px;
font-size: 13px;
- }
- """ + ARTIFACT_SCROLLBAR_STYLE)
+ }}
+ """ + artifact_scrollbar_style())
self.raw_editor.textChanged.connect(self._on_content_changed)
self.preview_display = QTextEdit()
self.preview_display.setReadOnly(True)
self.preview_display.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.preview_display.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
- self.preview_display.setStyleSheet("""
- QTextEdit {
- background-color: #1E1E1E;
+ self.preview_display.setStyleSheet(f"""
+ QTextEdit {{
+ background-color: {get_surface_color("window")};
border: none;
padding: 12px;
border-radius: 6px;
- }
- """ + ARTIFACT_SCROLLBAR_STYLE)
- self.preview_display.document().setDefaultStyleSheet("""
- p, ul, ol, li { color: #E0E0E0; font-family: 'Segoe UI', sans-serif; font-size: 13px; line-height: 1.5; margin-bottom: 10px; }
- h1, h2, h3, h4 { color: #FFFFFF; font-weight: bold; margin-top: 15px; margin-bottom: 8px; }
- pre { background-color: #2D2D2D; padding: 10px; border-radius: 6px; font-family: Consolas, monospace; color: #D8D8D8; }
- code { background-color: #3F3F3F; color: #D8D8D8; padding: 2px 4px; border-radius: 4px; font-family: Consolas, monospace; }
- blockquote { border-left: 3px solid #555555; padding-left: 10px; color: #AAAAAA; }
+ }}
+ """ + artifact_scrollbar_style())
+ self.preview_display.document().setDefaultStyleSheet(f"""
+ p, ul, ol, li {{ color: {get_surface_color("text_primary")}; font-family: {FONT_FAMILY}; font-size: 13px; line-height: 1.5; margin-bottom: 10px; }}
+ h1, h2, h3, h4 {{ color: {get_surface_color("text_bright")}; font-weight: bold; margin-top: 15px; margin-bottom: 8px; }}
+ pre {{ background-color: {get_surface_color("field")}; padding: 10px; border-radius: 6px; font-family: Consolas, monospace; color: {get_surface_color("text_primary")}; }}
+ code {{ background-color: {get_surface_color("border")}; color: {get_surface_color("text_primary")}; padding: 2px 4px; border-radius: 4px; font-family: Consolas, monospace; }}
+ blockquote {{ border-left: 3px solid {get_surface_color("handle")}; padding-left: 10px; color: {get_surface_color("text_label")}; }}
""")
self.tabs.addTab(self.raw_editor, "Markdown")
@@ -518,12 +520,12 @@ def set_running_state(self, is_running):
if is_running:
self.update_button.setEnabled(True)
self.update_button.setToolTip("Stop generation")
- self.update_button.setIcon(qta.icon('fa5s.stop', color='#DDDDDD'))
- self.update_button.setStyleSheet("QPushButton { background-color: #444444; border: none; border-radius: 20px; margin: 0px; } QPushButton:hover { background-color: #555555; }")
+ self.update_button.setIcon(qta.icon('fa5s.stop', color=get_surface_color("text_primary")))
+ self.update_button.setStyleSheet(f"QPushButton {{ background-color: {get_surface_color('divider')}; border: none; border-radius: 20px; margin: 0px; }} QPushButton:hover {{ background-color: {get_surface_color('handle')}; }}")
else:
artifact_color = get_semantic_color("artifact")
artifact_hover_color = artifact_color.lighter(115)
- artifact_button_icon = "#1E1E1E" if artifact_color.lightness() > 150 else "#F3F3F3"
+ artifact_button_icon = get_surface_color("window") if artifact_color.lightness() > 150 else get_surface_color("text_strong")
self.update_button.setEnabled(True)
self.update_button.setToolTip("")
self.update_button.setIcon(qta.icon('fa5s.arrow-up', color=artifact_button_icon))
@@ -541,16 +543,16 @@ def paint(self, painter, option, widget=None):
path = QPainterPath()
path.addRoundedRect(0, 0, self.width, self.height, 10, 10)
- painter.setBrush(QColor("#2D2D2D"))
-
+ painter.setBrush(QColor(get_surface_color("field")))
+
node_color = get_semantic_color("artifact")
pen = QPen(node_color, 1.5)
if self.isSelected():
pen = QPen(palette.SELECTION, 2)
elif self.hovered:
- pen = QPen(QColor("#FFFFFF"), 2)
-
+ pen = QPen(QColor(get_surface_color("text_bright")), 2)
+
painter.setPen(pen)
painter.drawPath(path)
@@ -586,7 +588,7 @@ def paint(self, painter, option, widget=None):
return
if self.is_collapsed:
- painter.setPen(QColor("#FFFFFF"))
+ painter.setPen(QColor(get_surface_color("text_bright")))
font = canvas_font(self.scene(), weight=QFont.Weight.Bold)
painter.setFont(font)
painter.drawText(QRectF(40, 0, self.width - 80, self.height), Qt.AlignmentFlag.AlignVCenter, "Artifact Drafter")
@@ -595,7 +597,7 @@ def paint(self, painter, option, widget=None):
icon.paint(painter, QRectF(10, 10, 20, 20).toRect())
self.collapse_button_rect = QRectF(self.width - 35, 5, 30, 30)
- expand_icon = qta.icon('fa5s.expand-arrows-alt', color='#FFFFFF' if self.hovered else '#888888')
+ expand_icon = qta.icon('fa5s.expand-arrows-alt', color=get_surface_color("text_bright") if self.hovered else get_surface_color("chrome_inactive"))
expand_icon.paint(painter, QRectF(self.width - 30, 10, 20, 20).toRect())
else:
if self.hovered:
@@ -604,7 +606,7 @@ def paint(self, painter, option, widget=None):
painter.setPen(QColor(255, 255, 255, 150))
painter.drawRoundedRect(self.collapse_button_rect.adjusted(6,6,-6,-6), 4, 4)
- icon_pen = QPen(QColor("#FFFFFF"), 2)
+ icon_pen = QPen(QColor(get_surface_color("text_bright")), 2)
painter.setPen(icon_pen)
center = self.collapse_button_rect.center()
painter.drawLine(int(center.x() - 4), int(center.y()), int(center.x() + 4), int(center.y()))
diff --git a/graphlink_app/graphlink_plugins/graphlink_plugin_code_sandbox.py b/graphlink_app/graphlink_plugins/graphlink_plugin_code_sandbox.py
index 46b3c03..0b6f831 100644
--- a/graphlink_app/graphlink_plugins/graphlink_plugin_code_sandbox.py
+++ b/graphlink_app/graphlink_plugins/graphlink_plugin_code_sandbox.py
@@ -24,35 +24,37 @@
from graphlink_agents_code_sandbox import SandboxStage
from graphlink_agents_pycoder import PyCoderStatus
from graphlink_canvas_items import HoverAnimationMixin
-from graphlink_config import canvas_font, get_current_palette, get_semantic_color
+from graphlink_config import canvas_font, get_current_palette, get_semantic_color, get_surface_color
+from graphlink_styles import FONT_FAMILY
from graphlink_connections import ConnectionItem
from graphlink_lod import draw_lod_card, preview_text, sync_proxy_render_state
from graphlink_plugins.graphlink_plugin_context_menu import PluginNodeContextMenu
from graphlink_pycoder import CodeEditor, PythonHighlighter, StatusItemWidget
-SANDBOX_SCROLLBAR_STYLE = """
- QScrollBar:vertical {
- background: #1E1E1E;
+def sandbox_scrollbar_style():
+ return f"""
+ QScrollBar:vertical {{
+ background: {get_surface_color("window")};
width: 10px;
margin: 0px;
border-radius: 5px;
- }
- QScrollBar::handle:vertical {
- background-color: #606060;
+ }}
+ QScrollBar::handle:vertical {{
+ background-color: {get_surface_color("handle")};
min-height: 24px;
border-radius: 5px;
- }
- QScrollBar::handle:vertical:hover {
- background-color: #797979;
- }
- QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
+ }}
+ QScrollBar::handle:vertical:hover {{
+ background-color: {get_surface_color("handle_hover")};
+ }}
+ QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0px;
background: none;
- }
- QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
+ }}
+ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{
background: none;
- }
+ }}
"""
@@ -78,15 +80,15 @@ def __init__(self, parent=None):
layout.addWidget(self.stages[SandboxStage.EXECUTE], 1, 0)
layout.addWidget(self.stages[SandboxStage.ANALYZE], 1, 1)
- self.setStyleSheet("""
- SandboxStatusTracker {
- background-color: #212121;
- border: 1px solid #3A3A3A;
+ self.setStyleSheet(f"""
+ SandboxStatusTracker {{
+ background-color: {get_surface_color("inset")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 8px;
- }
- QLabel {
+ }}
+ QLabel {{
background: transparent;
- }
+ }}
""")
def update_status(self, stage, status):
@@ -187,15 +189,15 @@ def __init__(self, parent_node, parent=None):
self.widget = QWidget()
self.widget.setObjectName("codeSandboxWidget")
self.widget.setFixedSize(self.NODE_WIDTH, self.NODE_HEIGHT)
- self.widget.setStyleSheet("""
- QWidget#codeSandboxWidget {
+ self.widget.setStyleSheet(f"""
+ QWidget#codeSandboxWidget {{
background: transparent;
- color: #E7E7E7;
- font-family: 'Segoe UI', sans-serif;
- }
- QWidget#codeSandboxWidget QLabel {
+ color: {get_surface_color("text_primary")};
+ font-family: {FONT_FAMILY};
+ }}
+ QWidget#codeSandboxWidget QLabel {{
background: transparent;
- }
+ }}
""")
self._setup_ui()
@@ -252,7 +254,7 @@ def _setup_ui(self):
subtitle_label = QLabel("Runs Python with per-node requirements in a virtualenv. Isolates installed packages only - not a security boundary; code runs with your full account privileges.")
subtitle_label.setWordWrap(True)
- subtitle_label.setStyleSheet("font-size: 11px; color: #A4A4A4;")
+ subtitle_label.setStyleSheet(f"font-size: 11px; color: {get_surface_color('text_label')};")
title_column.addWidget(subtitle_label)
header_layout.addLayout(title_column, 1)
@@ -277,7 +279,7 @@ def _setup_ui(self):
divider = QFrame()
divider.setFrameShape(QFrame.Shape.HLine)
- divider.setStyleSheet("background-color: #3A3A3A; border: none; height: 1px;")
+ divider.setStyleSheet(f"background-color: {get_surface_color('border')}; border: none; height: 1px;")
main_layout.addWidget(divider)
briefing_layout = QHBoxLayout()
@@ -290,12 +292,12 @@ def _setup_ui(self):
prompt_layout.setSpacing(8)
prompt_header = QLabel("Task Brief")
- prompt_header.setStyleSheet("font-size: 12px; font-weight: 700; color: #FFFFFF;")
+ prompt_header.setStyleSheet(f"font-size: 12px; font-weight: 700; color: {get_surface_color('text_bright')};")
prompt_layout.addWidget(prompt_header)
prompt_hint = QLabel("Describe what to build, test, analyze, or transform. The sandbox agent will generate code against the branch context.")
prompt_hint.setWordWrap(True)
- prompt_hint.setStyleSheet("font-size: 11px; color: #A4A4A4;")
+ prompt_hint.setStyleSheet(f"font-size: 11px; color: {get_surface_color('text_label')};")
prompt_layout.addWidget(prompt_hint)
self.prompt_input = QTextEdit()
@@ -303,17 +305,17 @@ def _setup_ui(self):
self.prompt_input.setFixedHeight(132)
self.prompt_input.setStyleSheet(f"""
QTextEdit {{
- background-color: #181818;
- border: 1px solid #393939;
+ background-color: {get_surface_color("window")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 8px;
padding: 10px;
- color: #F6F6F6;
+ color: {get_surface_color("text_bright")};
font-size: 12px;
}}
QTextEdit:focus {{
border: 1px solid {accent.name()};
}}
- {SANDBOX_SCROLLBAR_STYLE}
+ {sandbox_scrollbar_style()}
""")
self.prompt_input.textChanged.connect(self._on_prompt_changed)
prompt_layout.addWidget(self.prompt_input)
@@ -329,7 +331,7 @@ def _setup_ui(self):
deps_header_layout.setSpacing(8)
deps_header = QLabel("requirements.txt")
- deps_header.setStyleSheet("font-size: 12px; font-weight: 700; color: #FFFFFF;")
+ deps_header.setStyleSheet(f"font-size: 12px; font-weight: 700; color: {get_surface_color('text_bright')};")
deps_header_layout.addWidget(deps_header)
deps_header_layout.addStretch()
@@ -339,7 +341,7 @@ def _setup_ui(self):
deps_hint = QLabel("One dependency per line. The sandbox automatically rebuilds the environment when this manifest changes.")
deps_hint.setWordWrap(True)
- deps_hint.setStyleSheet("font-size: 11px; color: #A4A4A4;")
+ deps_hint.setStyleSheet(f"font-size: 11px; color: {get_surface_color('text_label')};")
deps_layout.addWidget(deps_hint)
self.requirements_input = QPlainTextEdit()
@@ -349,18 +351,18 @@ def _setup_ui(self):
self.requirements_input.textChanged.connect(self._on_requirements_changed)
self.requirements_input.setStyleSheet(f"""
QPlainTextEdit {{
- background-color: #181818;
- border: 1px solid #393939;
+ background-color: {get_surface_color("window")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 8px;
padding: 10px;
- color: #E0E0E0;
+ color: {get_surface_color("text_primary")};
font-family: Consolas, Monaco, monospace;
font-size: 12px;
}}
QPlainTextEdit:focus {{
border: 1px solid {accent.name()};
}}
- {SANDBOX_SCROLLBAR_STYLE}
+ {sandbox_scrollbar_style()}
""")
deps_layout.addWidget(self.requirements_input)
briefing_layout.addWidget(deps_card, 2)
@@ -393,15 +395,15 @@ def _setup_ui(self):
background-color: {accent.darker(115).name()};
}}
QPushButton:disabled {{
- background-color: #474747;
- color: #868686;
+ background-color: {get_surface_color("divider")};
+ color: {get_surface_color("chrome_inactive")};
}}
"""
self._default_secondary_style = f"""
QPushButton {{
- background-color: #252525;
- color: #E3E3E3;
- border: 1px solid #404040;
+ background-color: {get_surface_color("node_body")};
+ color: {get_surface_color("text_primary")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 8px;
padding: 10px 14px;
font-size: 12px;
@@ -409,28 +411,28 @@ def _setup_ui(self):
}}
QPushButton:hover {{
border: 1px solid {accent.name()};
- color: #FFFFFF;
+ color: {get_surface_color("text_bright")};
}}
QPushButton:pressed {{
- background-color: #1B1B1B;
+ background-color: {get_surface_color("window")};
}}
QPushButton:disabled {{
- background-color: #232323;
- color: #767676;
- border: 1px solid #373737;
+ background-color: {get_surface_color("node_body")};
+ color: {get_surface_color("text_muted")};
+ border: 1px solid {get_surface_color("border")};
}}
"""
self.generate_button.setStyleSheet(self._default_primary_style)
self.run_button.setStyleSheet(self._default_secondary_style)
self.generate_button.setIcon(qta.icon("fa5s.magic", color="white"))
- self.run_button.setIcon(qta.icon("fa5s.play", color="#E3E3E3"))
+ self.run_button.setIcon(qta.icon("fa5s.play", color=get_surface_color("text_primary")))
action_layout.addWidget(self.generate_button)
action_layout.addWidget(self.run_button)
action_hint = QLabel("Use the prompt for agent-driven generation, or run the current script directly with the declared dependencies.")
action_hint.setWordWrap(True)
- action_hint.setStyleSheet("font-size: 11px; color: #989898;")
+ action_hint.setStyleSheet(f"font-size: 11px; color: {get_surface_color('text_label')};")
action_layout.addWidget(action_hint, 1)
main_layout.addLayout(action_layout)
@@ -447,12 +449,12 @@ def _setup_ui(self):
code_header_layout.setSpacing(8)
code_header = QLabel("Sandbox Script")
- code_header.setStyleSheet("font-size: 12px; font-weight: 700; color: #FFFFFF;")
+ code_header.setStyleSheet(f"font-size: 12px; font-weight: 700; color: {get_surface_color('text_bright')};")
code_header_layout.addWidget(code_header)
code_header_layout.addStretch()
code_hint = QLabel("Editable execution file")
- code_hint.setStyleSheet("font-size: 11px; color: #989898;")
+ code_hint.setStyleSheet(f"font-size: 11px; color: {get_surface_color('text_label')};")
code_header_layout.addWidget(code_hint)
code_layout.addLayout(code_header_layout)
@@ -461,18 +463,18 @@ def _setup_ui(self):
self.code_input.setFixedHeight(250)
self.code_input.setStyleSheet(f"""
QPlainTextEdit {{
- background-color: #141414;
- border: 1px solid #393939;
+ background-color: {get_surface_color("window")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 8px;
padding: 8px;
- color: #E5E5E5;
+ color: {get_surface_color("text_primary")};
font-family: Consolas, Monaco, monospace;
font-size: 12px;
}}
QPlainTextEdit:focus {{
border: 1px solid {accent.name()};
}}
- {SANDBOX_SCROLLBAR_STYLE}
+ {sandbox_scrollbar_style()}
""")
self.code_input.textChanged.connect(self._on_code_changed)
code_layout.addWidget(self.code_input)
@@ -481,15 +483,15 @@ def _setup_ui(self):
self.results_tabs = QTabWidget()
self.results_tabs.setStyleSheet(f"""
QTabWidget::pane {{
- border: 1px solid #3A3A3A;
- background: #1B1B1B;
+ border: 1px solid {get_surface_color("border")};
+ background: {get_surface_color("window")};
border-radius: 8px;
}}
QTabBar::tab {{
- background: #232323;
- color: #A4A4A4;
+ background: {get_surface_color("node_body")};
+ color: {get_surface_color("text_label")};
padding: 8px 14px;
- border: 1px solid #3A3A3A;
+ border: 1px solid {get_surface_color("border")};
border-bottom: none;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
@@ -497,13 +499,13 @@ def _setup_ui(self):
font-weight: 700;
}}
QTabBar::tab:selected {{
- background: #1B1B1B;
- color: #FFFFFF;
+ background: {get_surface_color("window")};
+ color: {get_surface_color("text_bright")};
border-top: 2px solid {accent.name()};
}}
QTabBar::tab:hover:!selected {{
- background: #2A2A2A;
- color: #FFFFFF;
+ background: {get_surface_color("field")};
+ color: {get_surface_color("text_bright")};
}}
""")
@@ -512,16 +514,16 @@ def _setup_ui(self):
self.output_display.setPlaceholderText("Virtualenv setup, dependency install logs, and execution output will appear here...")
self.output_display.setStyleSheet(f"""
QTextEdit {{
- background-color: #121212;
- color: #CACACA;
+ background-color: {get_surface_color("inset_deep")};
+ color: {get_surface_color("text_soft")};
border: none;
padding: 12px;
font-family: Consolas, Monaco, monospace;
font-size: 11px;
}}
- {SANDBOX_SCROLLBAR_STYLE}
+ {sandbox_scrollbar_style()}
""")
- self.results_tabs.addTab(self.output_display, qta.icon("fa5s.terminal", color="#D2D2D2"), "Terminal")
+ self.results_tabs.addTab(self.output_display, qta.icon("fa5s.terminal", color=get_surface_color("text_soft")), "Terminal")
self.ai_analysis_display = QTextEdit()
self.ai_analysis_display.setReadOnly(True)
@@ -532,18 +534,18 @@ def _setup_ui(self):
border: none;
padding: 12px;
font-size: 12px;
- color: #ECECEC;
+ color: {get_surface_color("text_strong")};
}}
- {SANDBOX_SCROLLBAR_STYLE}
+ {sandbox_scrollbar_style()}
""")
- self.ai_analysis_display.document().setDefaultStyleSheet("""
- p, ul, ol, li { color: #ECECEC; font-family: 'Segoe UI', sans-serif; font-size: 13px; line-height: 1.5; }
- h1, h2, h3, h4 { color: #FFFFFF; font-weight: bold; margin-bottom: 5px; }
- pre { background-color: #292929; padding: 10px; border-radius: 6px; font-family: Consolas, monospace; color: #E1E1E1; }
- code { background-color: #353535; color: #E1E1E1; padding: 2px 4px; border-radius: 4px; font-family: Consolas, monospace; }
- blockquote { border-left: 3px solid #666666; padding-left: 10px; color: #B5B5B5; }
+ self.ai_analysis_display.document().setDefaultStyleSheet(f"""
+ p, ul, ol, li {{ color: {get_surface_color("text_strong")}; font-family: {FONT_FAMILY}; font-size: 13px; line-height: 1.5; }}
+ h1, h2, h3, h4 {{ color: {get_surface_color("text_bright")}; font-weight: bold; margin-bottom: 5px; }}
+ pre {{ background-color: {get_surface_color("field")}; padding: 10px; border-radius: 6px; font-family: Consolas, monospace; color: {get_surface_color("text_primary")}; }}
+ code {{ background-color: {get_surface_color("border")}; color: {get_surface_color("text_primary")}; padding: 2px 4px; border-radius: 4px; font-family: Consolas, monospace; }}
+ blockquote {{ border-left: 3px solid {get_surface_color("handle_hover")}; padding-left: 10px; color: {get_surface_color("text_secondary")}; }}
""")
- self.results_tabs.addTab(self.ai_analysis_display, qta.icon("fa5s.search", color="#D2D2D2"), "Review")
+ self.results_tabs.addTab(self.ai_analysis_display, qta.icon("fa5s.search", color=get_surface_color("text_soft")), "Review")
main_layout.addWidget(self.results_tabs, 1)
self.highlighter_code = PythonHighlighter(self.code_input.document())
@@ -553,12 +555,12 @@ def _setup_ui(self):
def _create_surface_card(self):
card = QWidget()
card.setObjectName("sandboxSurfaceCard")
- card.setStyleSheet("""
- QWidget#sandboxSurfaceCard {
- background-color: #1E1E1E;
- border: 1px solid #3A3A3A;
+ card.setStyleSheet(f"""
+ QWidget#sandboxSurfaceCard {{
+ background-color: {get_surface_color("window")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 10px;
- }
+ }}
""")
return card
@@ -629,13 +631,13 @@ def paint(self, painter, option, widget=None):
path = QPainterPath()
path.addRoundedRect(0, 0, self.width, self.height, 12, 12)
- painter.setBrush(QColor("#2B2B2B"))
+ painter.setBrush(QColor(get_surface_color("field")))
pen = QPen(accent, 1.6)
if self.isSelected():
pen = QPen(palette.SELECTION, 2.2)
elif self.hovered:
- pen = QPen(QColor("#FFFFFF"), 2.0)
+ pen = QPen(QColor(get_surface_color("text_bright")), 2.0)
painter.setPen(pen)
painter.drawPath(path)
@@ -681,14 +683,14 @@ def paint(self, painter, option, widget=None):
return
if self.is_collapsed:
- painter.setPen(QColor("#FFFFFF"))
+ painter.setPen(QColor(get_surface_color("text_bright")))
painter.setFont(canvas_font(self.scene(), weight=QFont.Weight.Bold))
painter.drawText(QRectF(42, 0, self.width - 84, self.height), Qt.AlignmentFlag.AlignVCenter, "Execution Sandbox")
qta.icon("fa5s.shield-alt", color=accent.name()).paint(painter, QRect(12, 11, 18, 18))
self.collapse_button_rect = QRectF(self.width - 34, 6, 28, 28)
expand_icon = qta.icon(
"fa5s.expand-arrows-alt",
- color="#FFFFFF" if self.hovered else "#979797",
+ color=get_surface_color("text_bright") if self.hovered else get_surface_color("text_label"),
)
expand_icon.paint(painter, QRect(int(self.width - 30), 10, 18, 18))
return
@@ -699,7 +701,7 @@ def paint(self, painter, option, widget=None):
painter.setPen(QColor(255, 255, 255, 120))
painter.drawRoundedRect(self.collapse_button_rect.adjusted(5, 5, -5, -5), 4, 4)
center = self.collapse_button_rect.center()
- painter.setPen(QPen(QColor("#FFFFFF"), 2))
+ painter.setPen(QPen(QColor(get_surface_color("text_bright")), 2))
painter.drawLine(int(center.x() - 4), int(center.y()), int(center.x() + 4), int(center.y()))
else:
self.collapse_button_rect = QRectF()
@@ -896,22 +898,22 @@ def set_running_state(self, is_running):
self.requirements_input.setReadOnly(is_running)
self.code_input.setReadOnly(is_running)
- stop_style = """
- QPushButton {
- background-color: #646464;
+ stop_style = f"""
+ QPushButton {{
+ background-color: {get_surface_color("handle_hover")};
color: white;
border: none;
border-radius: 8px;
padding: 10px 14px;
font-size: 12px;
font-weight: 700;
- }
- QPushButton:hover {
- background-color: #727272;
- }
- QPushButton:pressed {
- background-color: #535353;
- }
+ }}
+ QPushButton:hover {{
+ background-color: {get_surface_color("text_muted")};
+ }}
+ QPushButton:pressed {{
+ background-color: {get_surface_color("handle")};
+ }}
"""
if is_running:
@@ -931,7 +933,7 @@ def set_running_state(self, is_running):
self.generate_button.setStyleSheet(self._default_primary_style)
self.run_button.setStyleSheet(self._default_secondary_style)
self.generate_button.setIcon(qta.icon("fa5s.magic", color="white"))
- self.run_button.setIcon(qta.icon("fa5s.play", color="#E3E3E3"))
+ self.run_button.setIcon(qta.icon("fa5s.play", color=get_surface_color("text_primary")))
tone = "success" if self.status == "Ready" else "info"
self._update_status_pill(tone)
diff --git a/graphlink_app/graphlink_plugins/graphlink_plugin_gitlink.py b/graphlink_app/graphlink_plugins/graphlink_plugin_gitlink.py
index 875aa6c..d14d9bf 100644
--- a/graphlink_app/graphlink_plugins/graphlink_plugin_gitlink.py
+++ b/graphlink_app/graphlink_plugins/graphlink_plugin_gitlink.py
@@ -29,7 +29,8 @@
from graphlink_config import canvas_font
from graphlink_canvas_items import HoverAnimationMixin
-from graphlink_config import get_current_palette, get_semantic_color
+from graphlink_config import get_current_palette, get_semantic_color, get_surface_color
+from graphlink_styles import FONT_FAMILY
from graphlink_connections import ConnectionItem
from graphlink_plugins.graphlink_plugin_context_menu import PluginNodeContextMenu
from graphlink_plugins.common.github_client import GitHubRestClient
@@ -51,49 +52,50 @@
from graphlink_plugins.common.combo import PopupComboBox
-GITLINK_SCROLLBAR_STYLE = """
- QScrollBar:vertical {
- background: #1D1D1D;
+def gitlink_scrollbar_style():
+ return f"""
+ QScrollBar:vertical {{
+ background: {get_surface_color("window")};
width: 10px;
margin: 0px;
border-radius: 5px;
- }
- QScrollBar::handle:vertical {
- background-color: #5A5A5A;
+ }}
+ QScrollBar::handle:vertical {{
+ background-color: {get_surface_color("handle")};
min-height: 25px;
border-radius: 5px;
- }
- QScrollBar::handle:vertical:hover {
- background-color: #717171;
- }
- QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
+ }}
+ QScrollBar::handle:vertical:hover {{
+ background-color: {get_surface_color("handle_hover")};
+ }}
+ QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0px;
background: none;
- }
- QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
+ }}
+ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{
background: none;
- }
- QScrollBar:horizontal {
- background: #1D1D1D;
+ }}
+ QScrollBar:horizontal {{
+ background: {get_surface_color("window")};
height: 10px;
margin: 0px;
border-radius: 5px;
- }
- QScrollBar::handle:horizontal {
- background-color: #5A5A5A;
+ }}
+ QScrollBar::handle:horizontal {{
+ background-color: {get_surface_color("handle")};
min-width: 25px;
border-radius: 5px;
- }
- QScrollBar::handle:horizontal:hover {
- background-color: #717171;
- }
- QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
+ }}
+ QScrollBar::handle:horizontal:hover {{
+ background-color: {get_surface_color("handle_hover")};
+ }}
+ QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
width: 0px;
background: none;
- }
- QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
+ }}
+ QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {{
background: none;
- }
+ }}
"""
MAX_REPO_PAGES = 5
@@ -258,34 +260,34 @@ def _build_ui(self):
f"""
QWidget#gitlinkMainWidget {{
background-color: transparent;
- color: #E0E0E0;
- font-family: 'Segoe UI', sans-serif;
+ color: {get_surface_color("text_primary")};
+ font-family: {FONT_FAMILY};
}}
QWidget#gitlinkMainWidget QLabel {{
background-color: transparent;
}}
QFrame#gitlinkHeaderCard,
QFrame#gitlinkSectionCard {{
- background-color: #232323;
- border: 1px solid #3A3A3A;
+ background-color: {get_surface_color("node_body")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 10px;
}}
QLabel#gitlinkTitle {{
- color: #FFFFFF;
+ color: {get_surface_color("text_bright")};
font-size: 17px;
font-weight: bold;
}}
QLabel#gitlinkHint {{
- color: #A5A5A5;
+ color: {get_surface_color("text_label")};
font-size: 11px;
}}
QLabel#gitlinkFieldLabel {{
- color: #D6D6D6;
+ color: {get_surface_color("text_primary")};
font-size: 11px;
font-weight: bold;
}}
QLabel#gitlinkBadge {{
- color: #F3F3F3;
+ color: {get_surface_color("text_strong")};
background-color: rgba({badge_rgba}, 0.14);
border: 1px solid rgba({badge_rgba}, 0.28);
border-radius: 10px;
@@ -298,9 +300,9 @@ def _build_ui(self):
QListWidget,
QPlainTextEdit,
QTextEdit {{
- background-color: #1A1A1A;
- color: #F3F3F3;
- border: 1px solid #3A3A3A;
+ background-color: {get_surface_color("window")};
+ color: {get_surface_color("text_strong")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 8px;
padding: 6px 8px;
selection-background-color: {node_color.name()};
@@ -313,35 +315,35 @@ def _build_ui(self):
border-color: {node_color.name()};
}}
QPushButton {{
- background-color: #303030;
- color: #FFFFFF;
- border: 1px solid #474747;
+ background-color: {get_surface_color("field")};
+ color: {get_surface_color("text_bright")};
+ border: 1px solid {get_surface_color("divider")};
border-radius: 8px;
padding: 7px 12px;
font-weight: bold;
}}
QPushButton:hover {{
- background-color: #3E3E3E;
+ background-color: {get_surface_color("border")};
border-color: {node_color.name()};
}}
QPushButton:pressed {{
- background-color: #2E2E2E;
+ background-color: {get_surface_color("field")};
}}
QPushButton:disabled {{
- background-color: #262626;
- color: #848484;
- border-color: #353535;
+ background-color: {get_surface_color("node_body")};
+ color: {get_surface_color("chrome_inactive")};
+ border-color: {get_surface_color("border")};
}}
QTabWidget::pane {{
- border: 1px solid #3F3F3F;
- background: #202020;
+ border: 1px solid {get_surface_color("border")};
+ background: {get_surface_color("inset")};
border-radius: 8px;
}}
QTabBar::tab {{
- background: #282828;
- color: #A0A0A0;
+ background: {get_surface_color("field")};
+ color: {get_surface_color("text_label")};
padding: 7px 12px;
- border: 1px solid #3F3F3F;
+ border: 1px solid {get_surface_color("border")};
border-bottom: none;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
@@ -349,10 +351,10 @@ def _build_ui(self):
font-weight: bold;
}}
QTabBar::tab:selected {{
- background: #202020;
- color: #FFFFFF;
+ background: {get_surface_color("inset")};
+ color: {get_surface_color("text_bright")};
border-top: 2px solid {node_color.name()};
- border-bottom: 1px solid #202020;
+ border-bottom: 1px solid {get_surface_color("inset")};
}}
QListWidget::item {{
padding: 4px 6px;
@@ -360,7 +362,7 @@ def _build_ui(self):
}}
QListWidget::item:selected {{
background: rgba({badge_rgba}, 0.22);
- color: #FFFFFF;
+ color: {get_surface_color("text_bright")};
}}
"""
)
@@ -424,7 +426,7 @@ def _build_ui(self):
setup_scroll = QScrollArea()
setup_scroll.setWidgetResizable(True)
setup_scroll.setFrameShape(QFrame.Shape.NoFrame)
- setup_scroll.setStyleSheet(GITLINK_SCROLLBAR_STYLE)
+ setup_scroll.setStyleSheet(gitlink_scrollbar_style())
setup_layout.addWidget(setup_scroll)
setup_content = QWidget()
@@ -440,7 +442,7 @@ def _build_ui(self):
repo_layout.setSpacing(8)
repo_title = QLabel("GitHub Source")
- repo_title.setStyleSheet("color: #FFFFFF; font-weight: bold;")
+ repo_title.setStyleSheet(f"color: {get_surface_color('text_bright')}; font-weight: bold;")
repo_layout.addWidget(repo_title)
self.github_state_label = QLabel("")
@@ -453,7 +455,7 @@ def _build_ui(self):
repo_picker_row.setSpacing(8)
self.load_repos_button = QPushButton("Load Repo List")
- self.load_repos_button.setIcon(qta.icon("fa5s.sync-alt", color="#FFFFFF"))
+ self.load_repos_button.setIcon(qta.icon("fa5s.sync-alt", color=get_surface_color("text_bright")))
self.load_repos_button.clicked.connect(self.load_github_repositories)
repo_picker_row.addWidget(self.load_repos_button)
@@ -485,7 +487,7 @@ def _build_ui(self):
branch_row.addWidget(self.branch_input, stretch=1)
self.load_tree_button = QPushButton("Load File Tree")
- self.load_tree_button.setIcon(qta.icon("fa5s.sitemap", color="#FFFFFF"))
+ self.load_tree_button.setIcon(qta.icon("fa5s.sitemap", color=get_surface_color("text_bright")))
self.load_tree_button.clicked.connect(self.load_repository_tree)
branch_row.addWidget(self.load_tree_button)
repo_layout.addLayout(branch_row)
@@ -499,7 +501,7 @@ def _build_ui(self):
scope_layout.setSpacing(8)
scope_title = QLabel("Access Scope")
- scope_title.setStyleSheet("color: #FFFFFF; font-weight: bold;")
+ scope_title.setStyleSheet(f"color: {get_surface_color('text_bright')}; font-weight: bold;")
scope_layout.addWidget(scope_title)
scope_hint = QLabel(
@@ -544,7 +546,7 @@ def _build_ui(self):
self.file_list.setAlternatingRowColors(False)
self.file_list.itemSelectionChanged.connect(self._on_file_selection_changed)
self.file_list.setMinimumHeight(200)
- self.file_list.setStyleSheet(GITLINK_SCROLLBAR_STYLE)
+ self.file_list.setStyleSheet(gitlink_scrollbar_style())
scope_layout.addWidget(self.file_list)
setup_content_layout.addWidget(scope_card)
@@ -556,7 +558,7 @@ def _build_ui(self):
workspace_layout.setSpacing(8)
workspace_title = QLabel("Writable Checkout")
- workspace_title.setStyleSheet("color: #FFFFFF; font-weight: bold;")
+ workspace_title.setStyleSheet(f"color: {get_surface_color('text_bright')}; font-weight: bold;")
workspace_layout.addWidget(workspace_title)
workspace_hint = QLabel(
@@ -580,12 +582,12 @@ def _build_ui(self):
workspace_button_row.setSpacing(8)
self.browse_root_button = QPushButton("Browse")
- self.browse_root_button.setIcon(qta.icon("fa5s.folder-open", color="#FFFFFF"))
+ self.browse_root_button.setIcon(qta.icon("fa5s.folder-open", color=get_surface_color("text_bright")))
self.browse_root_button.clicked.connect(self.browse_local_root)
workspace_button_row.addWidget(self.browse_root_button)
self.import_repo_button = QPushButton("Import Repo Snapshot")
- self.import_repo_button.setIcon(qta.icon("fa5s.download", color="#FFFFFF"))
+ self.import_repo_button.setIcon(qta.icon("fa5s.download", color=get_surface_color("text_bright")))
self.import_repo_button.clicked.connect(self.import_repository_snapshot)
workspace_button_row.addWidget(self.import_repo_button)
workspace_button_row.addStretch()
@@ -600,7 +602,7 @@ def _build_ui(self):
task_layout.setSpacing(8)
task_title = QLabel("Task Prompt")
- task_title.setStyleSheet("color: #FFFFFF; font-weight: bold;")
+ task_title.setStyleSheet(f"color: {get_surface_color('text_bright')}; font-weight: bold;")
task_layout.addWidget(task_title)
task_hint = QLabel(
@@ -621,17 +623,17 @@ def _build_ui(self):
button_row.setSpacing(8)
self.build_context_button = QPushButton("Build XML Context")
- self.build_context_button.setIcon(qta.icon("fa5s.file-code", color="#FFFFFF"))
+ self.build_context_button.setIcon(qta.icon("fa5s.file-code", color=get_surface_color("text_bright")))
self.build_context_button.clicked.connect(self.build_context_preview)
button_row.addWidget(self.build_context_button)
self.run_button = QPushButton("Generate Change Set")
- self.run_button.setIcon(qta.icon("fa5s.magic", color="#FFFFFF"))
+ self.run_button.setIcon(qta.icon("fa5s.magic", color=get_surface_color("text_bright")))
self.run_button.clicked.connect(self._request_gitlink_run)
button_row.addWidget(self.run_button)
self.apply_button = QPushButton("Apply Approved Changes")
- self.apply_button.setIcon(qta.icon("fa5s.save", color="#FFFFFF"))
+ self.apply_button.setIcon(qta.icon("fa5s.save", color=get_surface_color("text_bright")))
self.apply_button.clicked.connect(self.apply_approved_changes)
self.apply_button.setEnabled(False)
button_row.addWidget(self.apply_button)
@@ -642,20 +644,20 @@ def _build_ui(self):
self.context_editor = QPlainTextEdit()
self.context_editor.setReadOnly(True)
- self.context_editor.setStyleSheet(GITLINK_SCROLLBAR_STYLE)
+ self.context_editor.setStyleSheet(gitlink_scrollbar_style())
self.proposal_display = QTextEdit()
self.proposal_display.setReadOnly(True)
- self.proposal_display.setStyleSheet(GITLINK_SCROLLBAR_STYLE)
+ self.proposal_display.setStyleSheet(gitlink_scrollbar_style())
self.preview_editor = QPlainTextEdit()
self.preview_editor.setReadOnly(True)
- self.preview_editor.setStyleSheet(GITLINK_SCROLLBAR_STYLE)
+ self.preview_editor.setStyleSheet(gitlink_scrollbar_style())
- self.workspace_tabs.addTab(setup_tab, qta.icon("fa5s.link", color="#CCCCCC"), "Setup")
- self.workspace_tabs.addTab(self.context_editor, qta.icon("fa5s.file-code", color="#CCCCCC"), "Context XML")
- self.workspace_tabs.addTab(self.proposal_display, qta.icon("fa5s.tasks", color="#CCCCCC"), "Proposal")
- self.workspace_tabs.addTab(self.preview_editor, qta.icon("fa5s.columns", color="#CCCCCC"), "Preview")
+ self.workspace_tabs.addTab(setup_tab, qta.icon("fa5s.link", color=get_surface_color("text_soft")), "Setup")
+ self.workspace_tabs.addTab(self.context_editor, qta.icon("fa5s.file-code", color=get_surface_color("text_soft")), "Context XML")
+ self.workspace_tabs.addTab(self.proposal_display, qta.icon("fa5s.tasks", color=get_surface_color("text_soft")), "Proposal")
+ self.workspace_tabs.addTab(self.preview_editor, qta.icon("fa5s.columns", color=get_surface_color("text_soft")), "Preview")
def _get_github_token(self):
# Delegates to the GitHubRestClient shared with Code Review - kept as a thin
@@ -1337,7 +1339,7 @@ def set_status(self, status_text):
elif "ready" in status_text.lower() or "applied" in status_text.lower():
color = get_semantic_color("status_success")
else:
- color = QColor("#A2A2A2")
+ color = QColor(get_surface_color("text_label"))
self.status_label.setStyleSheet(
f"color: {color.name()}; background-color: rgba({color.red()}, {color.green()}, {color.blue()}, 0.1); "
@@ -1424,7 +1426,7 @@ def paint(self, painter, option, widget=None):
path = QPainterPath()
path.addRoundedRect(0, 0, self.width, self.height, 10, 10)
- painter.setBrush(QColor("#2D2D2D"))
+ painter.setBrush(QColor(get_surface_color("field")))
pen = QPen(node_color, 1.5)
if self.isSelected():
@@ -1432,7 +1434,7 @@ def paint(self, painter, option, widget=None):
elif self.is_search_match:
pen = QPen(get_semantic_color("search_highlight"), 2)
elif self.hovered:
- pen = QPen(QColor("#FFFFFF"), 2)
+ pen = QPen(QColor(get_surface_color("text_bright")), 2)
painter.setPen(pen)
painter.drawPath(path)
@@ -1458,14 +1460,14 @@ def paint(self, painter, option, widget=None):
painter.drawPie(dot_rect_right, 90 * 16, 180 * 16)
if self.is_collapsed:
- painter.setPen(QColor("#FFFFFF"))
+ painter.setPen(QColor(get_surface_color("text_bright")))
painter.setFont(canvas_font(self.scene(), weight=QFont.Weight.Bold))
painter.drawText(QRectF(42, 0, self.width - 84, self.height), Qt.AlignmentFlag.AlignVCenter, "Gitlink")
qta.icon("fa5s.link", color=node_color.name()).paint(painter, QRect(12, 10, 20, 20))
self.collapse_button_rect = QRectF(self.width - 35, 5, 30, 30)
qta.icon(
"fa5s.expand-arrows-alt",
- color="#FFFFFF" if self.hovered else "#888888",
+ color=get_surface_color("text_bright") if self.hovered else get_surface_color("chrome_inactive"),
).paint(painter, QRect(int(self.width - 30), 10, 20, 20))
else:
if self.hovered:
@@ -1473,7 +1475,7 @@ def paint(self, painter, option, widget=None):
painter.setBrush(QColor(255, 255, 255, 30))
painter.setPen(QColor(255, 255, 255, 150))
painter.drawRoundedRect(self.collapse_button_rect.adjusted(6, 6, -6, -6), 4, 4)
- painter.setPen(QPen(QColor("#FFFFFF"), 2))
+ painter.setPen(QPen(QColor(get_surface_color("text_bright")), 2))
center = self.collapse_button_rect.center()
painter.drawLine(int(center.x() - 4), int(center.y()), int(center.x() + 4), int(center.y()))
else:
diff --git a/graphlink_app/graphlink_plugins/graphlink_plugin_portal.py b/graphlink_app/graphlink_plugins/graphlink_plugin_portal.py
index c1b6501..1227b28 100644
--- a/graphlink_app/graphlink_plugins/graphlink_plugin_portal.py
+++ b/graphlink_app/graphlink_plugins/graphlink_plugin_portal.py
@@ -7,7 +7,7 @@
SystemPromptConnectionItem, PyCoderConnectionItem, ConversationConnectionItem,
HtmlConnectionItem
)
-from graphlink_config import get_current_palette
+from graphlink_config import get_current_palette, get_surface_color
from graphlink_pycoder import PyCoderNode
from graphlink_plugins.graphlink_plugin_code_sandbox import CodeSandboxNode, CodeSandboxConnectionItem
from graphlink_node import ChatNode, CodeNode
@@ -415,7 +415,7 @@ def _create_system_prompt_node(self):
prompt_note.is_system_prompt = True
prompt_note.content = "Enter custom system prompt here..."
prompt_note.header_color = palette.FRAME_COLORS["Purple Header"]["color"]
- prompt_note.color = "#252525"
+ prompt_note.color = get_surface_color("node_body")
prompt_note.width = 300
prompt_note.height = 150
diff --git a/graphlink_app/graphlink_pycoder.py b/graphlink_app/graphlink_pycoder.py
index 54b259d..909504e 100644
--- a/graphlink_app/graphlink_pycoder.py
+++ b/graphlink_app/graphlink_pycoder.py
@@ -15,8 +15,9 @@
from PySide6.QtCore import QRectF, Qt, Signal, Property, QPropertyAnimation, QEasingCurve, QPointF, QRegularExpression, QSize, QRect
from PySide6.QtGui import QPainter, QColor, QBrush, QPen, QPainterPath, QIcon, QSyntaxHighlighter, QTextCharFormat, QFont
import qtawesome as qta
-from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_neutral_button_colors, get_semantic_color
+from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_neutral_button_colors, get_semantic_color, get_surface_color, get_syntax_color
from graphlink_lod import draw_lod_card, preview_text, sync_proxy_render_state
+from graphlink_styles import FONT_FAMILY
from graphlink_agents_pycoder import PyCoderStage, PyCoderStatus
from graphlink_canvas_items import HoverAnimationMixin
@@ -31,7 +32,7 @@ def __init__(self, document):
# Keywords
keyword_format = QTextCharFormat()
- keyword_format.setForeground(QColor("#909090")) # Purple
+ keyword_format.setForeground(QColor(get_syntax_color("keyword")))
keyword_format.setFontWeight(QFont.Weight.Bold)
keywords = [
"and", "as", "assert", "break", "class", "continue", "def",
@@ -46,7 +47,7 @@ def __init__(self, document):
# Builtins
builtin_format = QTextCharFormat()
- builtin_format.setForeground(QColor("#A2A2A2")) # Cyan
+ builtin_format.setForeground(QColor(get_syntax_color("builtin")))
builtins = ["print", "len", "range", "int", "float", "str", "list", "dict", "set", "tuple", "open", "type", "dir"]
for word in builtins:
pattern = QRegularExpression(rf"\b{word}\b")
@@ -54,24 +55,24 @@ def __init__(self, document):
# Numbers
number_format = QTextCharFormat()
- number_format.setForeground(QColor("#A2A2A2")) # Orange
+ number_format.setForeground(QColor(get_syntax_color("number")))
self.highlighting_rules.append((QRegularExpression(r"\b[0-9]+(?:\.[0-9]+)?\b"), number_format))
# Strings
string_format = QTextCharFormat()
- string_format.setForeground(QColor("#B5B5B5")) # Green
+ string_format.setForeground(QColor(get_syntax_color("string")))
self.highlighting_rules.append((QRegularExpression(r'".*?"'), string_format))
self.highlighting_rules.append((QRegularExpression(r"'.*?'"), string_format))
# Comments
comment_format = QTextCharFormat()
- comment_format.setForeground(QColor("#626262")) # Grey
+ comment_format.setForeground(QColor(get_syntax_color("comment")))
comment_format.setFontItalic(True)
self.highlighting_rules.append((QRegularExpression(r"#[^\n]*"), comment_format))
# Functions
function_format = QTextCharFormat()
- function_format.setForeground(QColor("#A3A3A3")) # Blue
+ function_format.setForeground(QColor(get_syntax_color("function")))
self.highlighting_rules.append((QRegularExpression(r"\b[A-Za-z0-9_]+(?=\()"), function_format))
def highlightBlock(self, text):
@@ -134,7 +135,7 @@ def resizeEvent(self, event):
def lineNumberAreaPaintEvent(self, event):
painter = QPainter(self.lineNumberArea)
- painter.fillRect(event.rect(), QColor("#1E1E1E").lighter(120))
+ painter.fillRect(event.rect(), QColor(get_surface_color("window")).lighter(120))
block = self.firstVisibleBlock()
blockNumber = block.blockNumber()
@@ -144,7 +145,7 @@ def lineNumberAreaPaintEvent(self, event):
while block.isValid() and top <= event.rect().bottom():
if block.isVisible() and bottom >= event.rect().top():
number = str(blockNumber + 1)
- painter.setPen(QColor("#6B6B6B"))
+ painter.setPen(QColor(get_surface_color("handle_hover")))
font = painter.font()
font.setFamily("Consolas")
painter.setFont(font)
@@ -193,7 +194,7 @@ def paintEvent(self, event):
palette = get_current_palette()
if self._status == PyCoderStatus.PENDING:
- painter.setPen(QPen(QColor("#555555"), 2))
+ painter.setPen(QPen(QColor(get_surface_color("handle")), 2))
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawEllipse(rect)
elif self._status == PyCoderStatus.RUNNING:
@@ -250,7 +251,7 @@ def __init__(self, text, parent=None):
self.icon_widget = StatusIconWidget()
self.text_label = QLabel(text)
- self.text_label.setStyleSheet("color: #888888; font-size: 11px;")
+ self.text_label.setStyleSheet(f"color: {get_surface_color('chrome_inactive')}; font-size: 11px;")
layout.addWidget(self.icon_widget)
layout.addWidget(self.text_label)
@@ -262,7 +263,7 @@ def set_status(self, status):
self.icon_widget.set_status(status)
palette = get_current_palette()
if status == PyCoderStatus.PENDING:
- self.text_label.setStyleSheet("color: #888888; font-style: italic; font-size: 11px;")
+ self.text_label.setStyleSheet(f"color: {get_surface_color('chrome_inactive')}; font-style: italic; font-size: 11px;")
elif status == PyCoderStatus.RUNNING:
self.text_label.setStyleSheet(f"color: {palette.AI_NODE.name()}; font-style: normal; font-weight: bold; font-size: 11px;")
elif status == PyCoderStatus.SUCCESS:
@@ -291,13 +292,13 @@ def __init__(self, parent=None):
layout.addWidget(self.stages[PyCoderStage.EXECUTE], 1, 0)
layout.addWidget(self.stages[PyCoderStage.ANALYZE_RESULT], 1, 1)
- self.setStyleSheet("""
- StatusTrackerWidget {
- background-color: #1E1E1E;
- border: 1px solid #3F3F3F;
+ self.setStyleSheet(f"""
+ StatusTrackerWidget {{
+ background-color: {get_surface_color("window")};
+ border: 1px solid {get_surface_color("border")};
border-radius: 6px;
- }
- QLabel { background: transparent; }
+ }}
+ QLabel {{ background: transparent; }}
""")
def update_status(self, stage, status):
@@ -372,15 +373,15 @@ def __init__(self, parent_node, mode=PyCoderMode.AI_DRIVEN, parent=None):
self.widget = QWidget()
self.widget.setObjectName("pyCoderMainWidget")
self.widget.setFixedSize(self.NODE_WIDTH, self.NODE_HEIGHT)
- self.widget.setStyleSheet("""
- QWidget#pyCoderMainWidget {
+ self.widget.setStyleSheet(f"""
+ QWidget#pyCoderMainWidget {{
background-color: transparent;
- color: #E0E0E0;
- font-family: 'Segoe UI', sans-serif;
- }
- QWidget#pyCoderMainWidget QLabel {
+ color: {get_surface_color("text_primary")};
+ font-family: {FONT_FAMILY};
+ }}
+ QWidget#pyCoderMainWidget QLabel {{
background-color: transparent;
- }
+ }}
""")
self._setup_ui()
@@ -506,16 +507,16 @@ def _setup_ui(self):
self.mode_toggle_button = QPushButton()
self.mode_toggle_button.setFixedSize(30, 30)
self.mode_toggle_button.clicked.connect(self._toggle_mode)
- self.mode_toggle_button.setStyleSheet("""
- QPushButton { border: 1px solid #555; border-radius: 15px; background-color: #2A2A2A; }
- QPushButton:hover { background-color: #4F4F4F; border: 1px solid #777; }
+ self.mode_toggle_button.setStyleSheet(f"""
+ QPushButton {{ border: 1px solid {get_surface_color("handle")}; border-radius: 15px; background-color: {get_surface_color("field")}; }}
+ QPushButton:hover {{ background-color: {get_surface_color("border_strong")}; border: 1px solid {get_surface_color("text_muted")}; }}
""")
header_layout.addWidget(self.mode_toggle_button)
main_layout.addLayout(header_layout)
line = QFrame()
line.setFrameShape(QFrame.Shape.HLine)
- line.setStyleSheet("background-color: #3F3F3F; border: none; height: 1px;")
+ line.setStyleSheet(f"background-color: {get_surface_color('border')}; border: none; height: 1px;")
main_layout.addWidget(line)
# --- AI-Driven Input Container ---
@@ -525,7 +526,7 @@ def _setup_ui(self):
prompt_layout.setSpacing(5)
self.ai_prompt_label = QLabel("Instruction Prompt:")
- self.ai_prompt_label.setStyleSheet("color: #AAAAAA; font-weight: bold; font-size: 12px; background: transparent;")
+ self.ai_prompt_label.setStyleSheet(f"color: {get_surface_color('text_label')}; font-weight: bold; font-size: 12px; background: transparent;")
prompt_layout.addWidget(self.ai_prompt_label)
self.prompt_input = QTextEdit()
@@ -533,14 +534,14 @@ def _setup_ui(self):
self.prompt_input.setFixedHeight(70)
prompt_focus_color = get_semantic_color("status_info").name()
prompt_focus_color = get_semantic_color("status_info").name()
- self.prompt_input.setStyleSheet("""
- QTextEdit {
- background-color: #1E1E1E; border: 1px solid #3F3F3F;
- border-radius: 6px; padding: 8px; color: #FFFFFF;
- font-family: 'Segoe UI', sans-serif;
- }
- QTextEdit:focus { border: 1px solid %s; }
- """ % prompt_focus_color)
+ self.prompt_input.setStyleSheet(f"""
+ QTextEdit {{
+ background-color: {get_surface_color("window")}; border: 1px solid {get_surface_color("border")};
+ border-radius: 6px; padding: 8px; color: {get_surface_color("text_bright")};
+ font-family: {FONT_FAMILY};
+ }}
+ QTextEdit:focus {{ border: 1px solid {prompt_focus_color}; }}
+ """)
self.prompt_input.textChanged.connect(self._on_prompt_changed)
prompt_layout.addWidget(self.prompt_input)
@@ -556,20 +557,20 @@ def _setup_ui(self):
manual_layout.setSpacing(5)
self.manual_code_label = QLabel("Python Code:")
- self.manual_code_label.setStyleSheet("color: #AAAAAA; font-weight: bold; font-size: 12px; background: transparent;")
+ self.manual_code_label.setStyleSheet(f"color: {get_surface_color('text_label')}; font-weight: bold; font-size: 12px; background: transparent;")
manual_layout.addWidget(self.manual_code_label)
self.code_input = CodeEditor()
self.code_input.setPlaceholderText("Enter Python code to execute...")
self.code_input.setFixedHeight(140)
- self.code_input.setStyleSheet("""
- QPlainTextEdit {
- background-color: #1E1E1E; border: 1px solid #3F3F3F;
- border-radius: 6px; padding: 8px; color: #D8D8D8;
+ self.code_input.setStyleSheet(f"""
+ QPlainTextEdit {{
+ background-color: {get_surface_color("window")}; border: 1px solid {get_surface_color("border")};
+ border-radius: 6px; padding: 8px; color: {get_surface_color("text_primary")};
font-family: Consolas, Monaco, monospace; font-size: 13px;
- }
- QPlainTextEdit:focus { border: 1px solid %s; }
- """ % prompt_focus_color)
+ }}
+ QPlainTextEdit:focus {{ border: 1px solid {prompt_focus_color}; }}
+ """)
self.code_input.textChanged.connect(self._on_code_changed)
manual_layout.addWidget(self.code_input)
@@ -597,9 +598,9 @@ def _setup_ui(self):
border-color: {button_colors["border"].darker(105).name()};
}}
QPushButton:disabled {{
- background-color: #2B2B2B;
- border-color: #353535;
- color: #7B7B7B;
+ background-color: {get_surface_color("field")};
+ border-color: {get_surface_color("border")};
+ color: {get_surface_color("text_muted")};
}}
"""
for btn in [self.generate_button, self.run_button]:
@@ -616,21 +617,21 @@ def _setup_ui(self):
self.tabs = QTabWidget()
self.tabs.setStyleSheet(f"""
QTabWidget::pane {{
- border: 1px solid #3F3F3F; background: #1E1E1E; border-radius: 6px;
+ border: 1px solid {get_surface_color("border")}; background: {get_surface_color("window")}; border-radius: 6px;
}}
QTabBar::tab {{
- background: #252525; color: #AAAAAA; padding: 8px 16px;
- border: 1px solid #3F3F3F; border-bottom: none;
+ background: {get_surface_color("node_body")}; color: {get_surface_color("text_label")}; padding: 8px 16px;
+ border: 1px solid {get_surface_color("border")}; border-bottom: none;
border-top-left-radius: 6px; border-top-right-radius: 6px;
margin-right: 2px; font-weight: bold;
}}
QTabBar::tab:selected {{
- background: #1E1E1E; color: #FFFFFF;
+ background: {get_surface_color("window")}; color: {get_surface_color("text_bright")};
border-top: 2px solid {pycoder_color.name()};
- border-bottom: 1px solid #1E1E1E;
+ border-bottom: 1px solid {get_surface_color("window")};
}}
QTabBar::tab:hover:!selected {{
- background: #2D2D2D; color: #FFFFFF;
+ background: {get_surface_color("field")}; color: {get_surface_color("text_bright")};
}}
""")
@@ -638,20 +639,20 @@ def _setup_ui(self):
self.generated_code_display = CodeEditor()
self.generated_code_display.setReadOnly(True)
self.generated_code_display.setPlaceholderText("Generated code will appear here...")
- self.generated_code_display.setStyleSheet("""
- QPlainTextEdit { background-color: transparent; color: #D8D8D8; font-family: Consolas, monospace; border: none; padding: 10px; font-size: 13px; }
+ self.generated_code_display.setStyleSheet(f"""
+ QPlainTextEdit {{ background-color: transparent; color: {get_surface_color("text_primary")}; font-family: Consolas, monospace; border: none; padding: 10px; font-size: 13px; }}
""")
- self.tabs.addTab(self.generated_code_display, qta.icon('fa5s.code', color='#ccc'), "Code")
+ self.tabs.addTab(self.generated_code_display, qta.icon('fa5s.code', color=get_surface_color("text_soft")), "Code")
# 2. Terminal / Output Tab
self.output_display = QTextEdit()
self.output_display.setReadOnly(True)
self.output_display.setPlaceholderText("Execution output will appear here...")
output_text_color = get_semantic_color("status_success").name()
- self.output_display.setStyleSheet("""
- QTextEdit { background-color: #0D0D0D; color: %s; font-family: Consolas, monospace; border: none; padding: 10px; font-size: 12px; }
- """ % output_text_color)
- self.tabs.addTab(self.output_display, qta.icon('fa5s.terminal', color='#ccc'), "Terminal")
+ self.output_display.setStyleSheet(f"""
+ QTextEdit {{ background-color: {get_surface_color("inset_deep")}; color: {output_text_color}; font-family: Consolas, monospace; border: none; padding: 10px; font-size: 12px; }}
+ """)
+ self.tabs.addTab(self.output_display, qta.icon('fa5s.terminal', color=get_surface_color("text_soft")), "Terminal")
# 3. AI Analysis Tab
self.ai_analysis_display = QTextEdit()
@@ -661,14 +662,14 @@ def _setup_ui(self):
QTextEdit { background-color: transparent; border: none; padding: 10px; }
""")
# Base styling for markdown content inside the text edit
- self.ai_analysis_display.document().setDefaultStyleSheet("""
- p, ul, ol, li { color: #E0E0E0; font-family: 'Segoe UI', sans-serif; font-size: 13px; line-height: 1.5; }
- h1, h2, h3, h4 { color: #FFFFFF; font-weight: bold; margin-bottom: 5px; }
- pre { background-color: #2D2D2D; padding: 10px; border-radius: 6px; font-family: Consolas, monospace; color: #D8D8D8; }
- code { background-color: #3F3F3F; color: #D8D8D8; padding: 2px 4px; border-radius: 4px; font-family: Consolas, monospace; }
- blockquote { border-left: 3px solid #555555; padding-left: 10px; color: #AAAAAA; }
+ self.ai_analysis_display.document().setDefaultStyleSheet(f"""
+ p, ul, ol, li {{ color: {get_surface_color("text_primary")}; font-family: {FONT_FAMILY}; font-size: 13px; line-height: 1.5; }}
+ h1, h2, h3, h4 {{ color: {get_surface_color("text_bright")}; font-weight: bold; margin-bottom: 5px; }}
+ pre {{ background-color: {get_surface_color("field")}; padding: 10px; border-radius: 6px; font-family: Consolas, monospace; color: {get_surface_color("text_primary")}; }}
+ code {{ background-color: {get_surface_color("border")}; color: {get_surface_color("text_primary")}; padding: 2px 4px; border-radius: 4px; font-family: Consolas, monospace; }}
+ blockquote {{ border-left: 3px solid {get_surface_color("handle")}; padding-left: 10px; color: {get_surface_color("text_label")}; }}
""")
- self.tabs.addTab(self.ai_analysis_display, qta.icon('fa5s.robot', color='#ccc'), "Analysis")
+ self.tabs.addTab(self.ai_analysis_display, qta.icon('fa5s.robot', color=get_surface_color("text_soft")), "Analysis")
main_layout.addWidget(self.tabs)
@@ -692,13 +693,13 @@ def _update_ui_for_mode(self):
if is_ai_mode:
self.title_label.setText("Py-Coder (AI-Driven)")
- self.mode_toggle_button.setIcon(qta.icon('fa5s.user-edit', color='#ccc'))
+ self.mode_toggle_button.setIcon(qta.icon('fa5s.user-edit', color=get_surface_color("text_soft")))
self.mode_toggle_button.setToolTip("Switch to Manual Mode")
self.tabs.setTabVisible(0, True) # Show Code tab
self.tabs.setCurrentIndex(0)
else:
self.title_label.setText("Py-Coder (Manual)")
- self.mode_toggle_button.setIcon(qta.icon('fa5s.magic', color='#ccc'))
+ self.mode_toggle_button.setIcon(qta.icon('fa5s.magic', color=get_surface_color("text_soft")))
self.mode_toggle_button.setToolTip("Switch to AI-Driven Mode")
self.tabs.setTabVisible(0, False) # Hide Code tab (it's in the top input box now)
self.tabs.setCurrentIndex(1) # Default to Terminal tab
@@ -720,19 +721,19 @@ def paint(self, painter, option, widget=None):
path = QPainterPath()
path.addRoundedRect(0, 0, self.width, self.height, 10, 10)
- painter.setBrush(QColor("#2D2D2D"))
-
+ painter.setBrush(QColor(get_surface_color("field")))
+
pycoder_color = node_colors["border"]
pen = QPen(pycoder_color, 1.5)
if self.isSelected():
pen = QPen(palette.SELECTION, 2)
elif self.hovered:
- pen = QPen(QColor("#FFFFFF"), 2)
-
+ pen = QPen(QColor(get_surface_color("text_bright")), 2)
+
painter.setPen(pen)
painter.drawPath(path)
-
+
dot_color = node_colors["dot"]
if self.isSelected() or self.hovered:
dot_color = pen.color().lighter(110) if self.isSelected() else node_colors["hover_dot"]
@@ -765,7 +766,7 @@ def paint(self, painter, option, widget=None):
return
if self.is_collapsed:
- painter.setPen(QColor("#FFFFFF"))
+ painter.setPen(QColor(get_surface_color("text_bright")))
font = canvas_font(self.scene(), weight=QFont.Weight.Bold)
painter.setFont(font)
title = f"Py-Coder ({'AI' if self.mode == PyCoderMode.AI_DRIVEN else 'Manual'})"
@@ -775,7 +776,7 @@ def paint(self, painter, option, widget=None):
icon.paint(painter, QRect(10, 10, 20, 20))
self.collapse_button_rect = QRectF(self.width - 35, 5, 30, 30)
- expand_icon = qta.icon('fa5s.expand-arrows-alt', color='#FFFFFF' if self.hovered else '#888888')
+ expand_icon = qta.icon('fa5s.expand-arrows-alt', color=get_surface_color("text_bright") if self.hovered else get_surface_color("chrome_inactive"))
expand_icon.paint(painter, QRect(int(self.width - 30), 10, 20, 20))
else:
if self.hovered:
@@ -784,7 +785,7 @@ def paint(self, painter, option, widget=None):
painter.setPen(QColor(255, 255, 255, 150))
painter.drawRoundedRect(self.collapse_button_rect.adjusted(6,6,-6,-6), 4, 4)
- icon_pen = QPen(QColor("#FFFFFF"), 2)
+ icon_pen = QPen(QColor(get_surface_color("text_bright")), 2)
painter.setPen(icon_pen)
center = self.collapse_button_rect.center()
painter.drawLine(int(center.x() - 4), int(center.y()), int(center.x() + 4), int(center.y()))
diff --git a/graphlink_app/graphlink_scene.py b/graphlink_app/graphlink_scene.py
index ec9d4ba..965c061 100644
--- a/graphlink_app/graphlink_scene.py
+++ b/graphlink_app/graphlink_scene.py
@@ -21,6 +21,8 @@
from graphlink_plugins.graphlink_plugin_artifact import ArtifactNode, ArtifactConnectionItem
from graphlink_plugins.graphlink_plugin_gitlink import GitlinkNode, GitlinkConnectionItem
from graphlink_memory import clone_history, resolve_branch_parent
+from graphlink_config import get_surface_color
+from graphlink_styles import FONT_FAMILY_NAME
class ChatScene(QGraphicsScene):
BRANCH_DIM_OPACITY = 0.18
@@ -85,7 +87,7 @@ def __init__(self, window):
self.artifact_connections = []
self.gitlink_connections = []
- self.setBackgroundBrush(QColor("#252525"))
+ self.setBackgroundBrush(QColor(get_surface_color("node_body")))
# Parameters for the auto-layout algorithm.
self.horizontal_spacing = 150
@@ -102,9 +104,9 @@ def __init__(self, window):
self.is_rubber_band_dragging = False
# Global font properties for nodes that support them.
- self.font_family = "Segoe UI"
+ self.font_family = FONT_FAMILY_NAME
self.font_size = 10
- self.font_color = QColor("#DDDDDD")
+ self.font_color = QColor(get_surface_color("text_primary"))
def setFontFamily(self, family):
"""
diff --git a/graphlink_app/graphlink_session/deserializers.py b/graphlink_app/graphlink_session/deserializers.py
index dc131db..ee0bc2e 100644
--- a/graphlink_app/graphlink_session/deserializers.py
+++ b/graphlink_app/graphlink_session/deserializers.py
@@ -3,6 +3,7 @@
from PySide6.QtCore import QPointF, QRectF
from PySide6.QtGui import QTransform
+from graphlink_canvas.graphlink_canvas_container import DEFAULT_CONTAINER_COLOR
from graphlink_canvas_items import Container, Frame
from graphlink_connections import (
ConnectionItem,
@@ -466,7 +467,7 @@ def deserialize_container(self, data, scene, all_items_map):
container.persistent_id = data["id"]
container.setPos(data["position"]["x"], data["position"]["y"])
container.title = data.get("title", "Container")
- container.color = data.get("color", "#3a3a3a")
+ container.color = data.get("color", DEFAULT_CONTAINER_COLOR)
container.header_color = data.get("header_color")
rect_data = data.get("expanded_rect")
diff --git a/graphlink_app/graphlink_styles.py b/graphlink_app/graphlink_styles.py
index 0c0e5de..258ca01 100644
--- a/graphlink_app/graphlink_styles.py
+++ b/graphlink_app/graphlink_styles.py
@@ -797,6 +797,44 @@ class StyleSheet:
"badge_fill": "#484848",
"panel_fill": "#202020",
},
+ # UI-refactor P0 (doc/UI_QA_AUDIT.md section 7): the clean neutral
+ # role set for native painting/stylesheet code. Values are the exact
+ # hexes that dominated the swept files (node cards, canvas items,
+ # widget chrome) so the migration is byte-neutral in this theme;
+ # mono/muted values are derived from each theme's existing qss
+ # neutrals so themed rendering stays coherent.
+ "surface": {
+ "window": "#1E1E1E",
+ "node_body": "#252525",
+ "inset": "#202020",
+ "inset_deep": "#121212",
+ "field": "#272727",
+ "border": "#3F3F3F",
+ "border_strong": "#505050",
+ "divider": "#424242",
+ "handle": "#555555",
+ "handle_hover": "#6A6A6A",
+ "chrome_inactive": "#888888",
+ "text_primary": "#E0E0E0",
+ "text_strong": "#F1F1F1",
+ "text_soft": "#CCCCCC",
+ "text_label": "#A4A4A4",
+ "text_secondary": "#BDBDBD",
+ "text_muted": "#767676",
+ "text_bright": "#FFFFFF",
+ },
+ # Sweep-adjudicated (2026-07-22): PythonHighlighter's palette moved
+ # here from graphlink_pycoder.py so code-node syntax colors theme with
+ # everything else. Identical across themes today - a deliberate
+ # starting point, not a constraint.
+ "syntax": {
+ "keyword": "#909090",
+ "builtin": "#A2A2A2",
+ "number": "#A2A2A2",
+ "string": "#B5B5B5",
+ "comment": "#626262",
+ "function": "#A3A3A3",
+ },
"qss": {
"qmainwindow_qwidget__background_color": "#1E1E1E",
"qmainwindow_qwidget__color": "#DCDCDC",
@@ -895,6 +933,34 @@ class StyleSheet:
"badge_fill": "#484848",
"panel_fill": "#202020",
},
+ "surface": {
+ "window": "#222222",
+ "node_body": "#292929",
+ "inset": "#202020",
+ "inset_deep": "#161616",
+ "field": "#2A2A2A",
+ "border": "#444444",
+ "border_strong": "#555555",
+ "divider": "#444444",
+ "handle": "#555555",
+ "handle_hover": "#6A6A6A",
+ "chrome_inactive": "#999999",
+ "text_primary": "#DDDDDD",
+ "text_strong": "#F1F1F1",
+ "text_soft": "#CFCFCF",
+ "text_label": "#ABABAB",
+ "text_secondary": "#D5D5D5",
+ "text_muted": "#8A8A8A",
+ "text_bright": "#FFFFFF",
+ },
+ "syntax": {
+ "keyword": "#909090",
+ "builtin": "#A2A2A2",
+ "number": "#A2A2A2",
+ "string": "#B5B5B5",
+ "comment": "#626262",
+ "function": "#A3A3A3",
+ },
"qss": {
"qmainwindow_qwidget__background_color": "#222222",
"qmainwindow_qwidget__color": "#DDDDDD",
@@ -985,6 +1051,34 @@ class StyleSheet:
"badge_fill": "#4a4a4a",
"panel_fill": "#1c1c1c",
},
+ "surface": {
+ "window": "#1A1A1A",
+ "node_body": "#222222",
+ "inset": "#1C1C1C",
+ "inset_deep": "#101010",
+ "field": "#232323",
+ "border": "#383838",
+ "border_strong": "#494949",
+ "divider": "#383838",
+ "handle": "#494949",
+ "handle_hover": "#5E5E5E",
+ "chrome_inactive": "#7E7E7E",
+ "text_primary": "#D1D1D1",
+ "text_strong": "#E8E8E8",
+ "text_soft": "#C4C4C4",
+ "text_label": "#9C9C9C",
+ "text_secondary": "#BABABA",
+ "text_muted": "#6E6E6E",
+ "text_bright": "#F5F5F5",
+ },
+ "syntax": {
+ "keyword": "#909090",
+ "builtin": "#A2A2A2",
+ "number": "#A2A2A2",
+ "string": "#B5B5B5",
+ "comment": "#626262",
+ "function": "#A3A3A3",
+ },
"qss": {
"qmainwindow_qwidget__background_color": "#1A1A1A",
"qmainwindow_qwidget__color": "#D1D1D1",
@@ -1096,6 +1190,87 @@ def _generate_qss(theme_name: str) -> str:
# so this is "the app's one explicit font choice," not "every font in use."
FONT_FAMILY = "'Segoe UI', sans-serif"
+# Bare family name for QFont construction (QFont wants "Segoe UI", not the
+# CSS-quoted stack above). UI-refactor P0: every widget/painting file that
+# used to string-paste "Segoe UI" imports this instead - the acceptance gate
+# (tests/test_ui_token_acceptance.py) enforces that this module is the only
+# place the literal appears.
+FONT_FAMILY_NAME = "Segoe UI"
+
+# ---------------------------------------------------------------------------
+# UI-refactor P0: structure tokens (doc/UI_QA_AUDIT.md section 7, P0).
+#
+# Theme-INDEPENDENT scales - spacing on a 4px grid, three radii, a type ramp
+# with nothing under 12px (audit finding D2: 9-10px microtext), three
+# elevation levels (audit finding B9: no elevation model), and motion
+# durations/easing (audit finding F4: zero transitions). One source, two
+# consumers: css_custom_properties() flattens these into --gl-* custom
+# properties for every island (and tailwind_theme_css() registers them under
+# Tailwind's proper non-color namespaces), while the *_PX/ELEVATION_PARAMS
+# accessors below give Qt-side code integer values for the same scales so
+# native painting/QSS derives from the identical source of truth.
+#
+# RECORDED DECISION (2026-07-22): the audit's P0 text sketched "one token
+# source (JSON)". This codebase already HAS the single token source - this
+# module: THEME_TOKENS feeds runtime island injection (css_root_block ->
+# _inline_bundle) and the generated Tailwind theme. A parallel JSON file
+# would fork truth into two places, so P0 extends THIS module instead; the
+# audit doc's P0 entry is updated to match. "Token modules" for the
+# acceptance grep = this file alone.
+STRUCTURE_TOKENS = {
+ "space": {
+ "1": "4px",
+ "2": "8px",
+ "3": "12px",
+ "4": "16px",
+ "5": "20px",
+ "6": "24px",
+ "8": "32px",
+ },
+ "radius": {
+ "sm": "4px",
+ "md": "8px",
+ "lg": "12px",
+ },
+ "text": {
+ "xs": "12px",
+ "sm": "13px",
+ "base": "14px",
+ "lg": "16px",
+ "xl": "20px",
+ },
+ "weight": {
+ "regular": "400",
+ "semibold": "600",
+ },
+ "shadow": {
+ "1": "0 1px 3px rgba(0, 0, 0, 0.40)",
+ "2": "0 4px 12px rgba(0, 0, 0, 0.45)",
+ "3": "0 8px 28px rgba(0, 0, 0, 0.55)",
+ },
+ "motion": {
+ "fast": "150ms",
+ "base": "200ms",
+ "ease": "cubic-bezier(0.2, 0, 0, 1)",
+ },
+}
+
+# Qt-side integer views of the same scales (QFont/QMargins/QRect math wants
+# ints, not "13px" strings). Derived, not duplicated: parsed from
+# STRUCTURE_TOKENS so the two representations cannot drift.
+SPACE_PX = {int(key): int(value[:-2]) for key, value in STRUCTURE_TOKENS["space"].items()}
+RADIUS_PX = {key: int(value[:-2]) for key, value in STRUCTURE_TOKENS["radius"].items()}
+TEXT_PX = {key: int(value[:-2]) for key, value in STRUCTURE_TOKENS["text"].items()}
+
+# QGraphicsDropShadowEffect parameters (blur_radius, y_offset, alpha) matching
+# the three CSS shadow levels closely enough that a native popover and a web
+# popover read as the same elevation tier on screen.
+ELEVATION_PARAMS = {
+ 1: (12, 2, 110),
+ 2: (24, 4, 120),
+ 3: (40, 8, 140),
+}
+
# THEME_TOKENS groups that belong to ONE island's own chrome rather than to the
# app-wide color vocabulary, mapped to the CSS custom-property prefix they
# flatten under. These are exported by css_custom_properties() (island CSS has
@@ -1196,7 +1371,7 @@ def css_custom_properties(theme_name: str) -> dict[str, str]:
function already exports.
"""
tokens = THEME_TOKENS[theme_name]
- included_groups = ("palette", "semantic", "neutral_button", "graph_node")
+ included_groups = ("palette", "semantic", "neutral_button", "graph_node", "surface", "syntax")
excluded_groups = ("qss", "qss_alpha")
# Self-verifying rather than relying solely on a separate test file to
# catch drift: if a future edit adds a new top-level THEME_TOKENS group
@@ -1260,6 +1435,14 @@ def css_custom_properties(theme_name: str) -> dict[str, str]:
properties["--gl-font-family"] = FONT_FAMILY
+ # UI-refactor P0: theme-independent structure scales, emitted identically
+ # for every theme (so the cross-theme key-set guard holds by
+ # construction). Names: --gl-space-N, --gl-radius-K, --gl-text-K,
+ # --gl-weight-K, --gl-shadow-N, --gl-motion-K.
+ for group, entries in STRUCTURE_TOKENS.items():
+ for key, value in entries.items():
+ properties[f"--gl-{group}-{key}"] = value
+
for name, value in properties.items():
_assert_safe_css_declaration_value(name, value)
return properties
@@ -1332,12 +1515,43 @@ def tailwind_theme_css() -> str:
"""
excluded = island_property_names("dark")
names = [name for name in sorted(css_custom_properties("dark")) if name not in excluded]
+ # UI-refactor P0: structure tokens register under Tailwind v4's proper
+ # non-color namespaces so the compiler mints the RIGHT kind of utility
+ # (p-gl-2 / rounded-gl-md / text-gl-sm / shadow-gl-1 / duration-gl-fast),
+ # not a bogus bg-gl-space-2 color utility. Routed by emitted-name prefix;
+ # prefixes are derived from STRUCTURE_TOKENS's real group names so a new
+ # group cannot silently fall through to the color namespace.
+ structure_namespace = {
+ "space": "--spacing-gl-",
+ "radius": "--radius-gl-",
+ "text": "--text-gl-",
+ "weight": "--font-weight-gl-",
+ "shadow": "--shadow-gl-",
+ }
+ assert set(structure_namespace) | {"motion"} == set(STRUCTURE_TOKENS), (
+ "STRUCTURE_TOKENS gained a group tailwind_theme_css() doesn't route - "
+ "add it to structure_namespace (or the motion special-case) before "
+ "extending the scales."
+ )
lines = []
for name in names:
if name == "--gl-font-family":
lines.append(f" --font-gl: var({name});")
+ continue
+ for group, tw_prefix in structure_namespace.items():
+ gl_prefix = f"--gl-{group}-"
+ if name.startswith(gl_prefix):
+ lines.append(f" {tw_prefix}{name[len(gl_prefix):]}: var({name});")
+ break
else:
- lines.append(f" --color-{name[len('--'):]}: var({name});")
+ if name.startswith("--gl-motion-"):
+ key = name[len("--gl-motion-"):]
+ if key == "ease":
+ lines.append(f" --ease-gl: var({name});")
+ else:
+ lines.append(f" --duration-gl-{key}: var({name});")
+ else:
+ lines.append(f" --color-{name[len('--'):]}: var({name});")
return "@theme {\n" + "\n".join(lines) + "\n}\n"
diff --git a/graphlink_app/graphlink_ui_dialogs/graphlink_library_dialog.py b/graphlink_app/graphlink_ui_dialogs/graphlink_library_dialog.py
index 6e29297..f4cbd3a 100644
--- a/graphlink_app/graphlink_ui_dialogs/graphlink_library_dialog.py
+++ b/graphlink_app/graphlink_ui_dialogs/graphlink_library_dialog.py
@@ -12,7 +12,7 @@
)
from graphlink_chat_library_web import ChatLibraryWebHost
-from graphlink_config import get_current_palette
+from graphlink_config import get_current_palette, get_surface_color
class ChatLibraryDialog(QDialog):
@@ -149,7 +149,7 @@ def on_theme_changed(self):
accent = palette.SELECTION.name()
panel_gray = "rgba(42, 42, 42, 248)"
line_gray = "rgba(255, 255, 255, 0.08)"
- muted_text = "#8D8D8D"
+ muted_text = get_surface_color("chrome_inactive")
badge_gray = "rgba(255, 255, 255, 0.025)"
self.icon_badge.setPixmap(qta.icon("fa5s.book", color=accent).pixmap(16, 16))
@@ -181,7 +181,7 @@ def on_theme_changed(self):
letter-spacing: 0.14em;
}}
QLabel#libraryWindowTitle {{
- color: #F5F5F5;
+ color: {get_surface_color("text_strong")};
font-size: 16px;
font-weight: 700;
}}
@@ -191,7 +191,7 @@ def on_theme_changed(self):
}}
QPushButton#libraryCloseButton {{
background-color: rgba(255, 255, 255, 0.04);
- color: #F5F5F5;
+ color: {get_surface_color("text_strong")};
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 8px 14px;
diff --git a/graphlink_app/graphlink_view.py b/graphlink_app/graphlink_view.py
index 3c80b03..5e8007d 100644
--- a/graphlink_app/graphlink_view.py
+++ b/graphlink_app/graphlink_view.py
@@ -15,7 +15,7 @@
from graphlink_font_control_web import FontControlHost
from graphlink_drag_speed_web import DragSpeedHost
from graphlink_minimap_web import MinimapHost
-from graphlink_config import get_semantic_color
+from graphlink_config import get_semantic_color, get_surface_color
from graphlink_context_menu import create_context_menu
from graphlink_web_island_host import any_host_has_text_focus
@@ -677,7 +677,7 @@ def drawBackground(self, painter, rect):
"""
super().drawBackground(painter, rect)
- painter.fillRect(rect, QColor("#252525"))
+ painter.fillRect(rect, QColor(get_surface_color("node_body")))
grid_size = self.grid_settings.grid_size
opacity = self.grid_settings.grid_opacity
diff --git a/graphlink_app/graphlink_web.py b/graphlink_app/graphlink_web.py
index a578738..4ebabc2 100644
--- a/graphlink_app/graphlink_web.py
+++ b/graphlink_app/graphlink_web.py
@@ -5,9 +5,10 @@
from PySide6.QtCore import QRectF, Qt, QPointF, Signal, QTimer, QRect, QUrl
from PySide6.QtGui import QPainter, QColor, QBrush, QPen, QPainterPath, QFont, QDesktopServices
import qtawesome as qta
-from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_neutral_button_colors, get_semantic_color
+from graphlink_config import canvas_font, get_current_palette, get_graph_node_colors, get_neutral_button_colors, get_semantic_color, get_surface_color
from graphlink_connections import ConnectionItem
from graphlink_canvas_items import HoverAnimationMixin
+from graphlink_styles import FONT_FAMILY
from graphlink_lod import draw_lod_card, preview_text, sync_proxy_render_state
from graphlink_memory import append_history, get_node_history
from graphlink_plugins.graphlink_plugin_context_menu import PluginNodeContextMenu
@@ -141,14 +142,14 @@ def __init__(self, parent_node, parent=None):
self.widget = QWidget()
self.widget.setObjectName("webNodeMainWidget")
self.widget.setFixedSize(self.NODE_WIDTH, self.NODE_HEIGHT)
- self.widget.setStyleSheet("""
- QWidget#webNodeMainWidget {
+ self.widget.setStyleSheet(f"""
+ QWidget#webNodeMainWidget {{
background-color: transparent;
- color: #E0E0E0;
- }
- QWidget#webNodeMainWidget QLabel {
+ color: {get_surface_color("text_primary")};
+ }}
+ QWidget#webNodeMainWidget QLabel {{
background-color: transparent;
- }
+ }}
""")
self._setup_ui()
@@ -222,14 +223,14 @@ def _setup_ui(self):
# --- Status Label ---
self.status_label = QLabel("Status: Idle")
- self.status_label.setStyleSheet("color: #888; font-style: italic; background: transparent;")
+ self.status_label.setStyleSheet(f"color: {get_surface_color('chrome_inactive')}; font-style: italic; background: transparent;")
main_layout.addWidget(self.status_label)
# --- Result Display Section ---
result_header = QHBoxLayout()
result_header.addWidget(QLabel("Answer:"))
self.source_count_label = QLabel("No sources")
- self.source_count_label.setStyleSheet("color: #A4A4A4; background: transparent;")
+ self.source_count_label.setStyleSheet(f"color: {get_surface_color('text_label')}; background: transparent;")
result_header.addWidget(self.source_count_label)
result_header.addStretch()
main_layout.addLayout(result_header)
@@ -244,18 +245,18 @@ def _setup_ui(self):
self.warning_label = QLabel("")
self.warning_label.setWordWrap(True)
- self.warning_label.setStyleSheet("color: #AAAAAA; background: transparent;")
+ self.warning_label.setStyleSheet(f"color: {get_surface_color('text_label')}; background: transparent;")
main_layout.addWidget(self.warning_label)
# Apply common styles to text edit widgets.
for widget in [self.query_input, self.summary_display]:
- widget.setStyleSheet("""
- QTextEdit, QTextBrowser {
- background-color: #252525; border: 1px solid #3F3F3F;
- color: #CCCCCC; border-radius: 4px; padding: 5px;
- font-family: 'Segoe UI', sans-serif;
- }
- QTextBrowser a { color: #B3B3B3; text-decoration: none; }
+ widget.setStyleSheet(f"""
+ QTextEdit, QTextBrowser {{
+ background-color: {get_surface_color("node_body")}; border: 1px solid {get_surface_color("border")};
+ color: {get_surface_color("text_soft")}; border-radius: 4px; padding: 5px;
+ font-family: {FONT_FAMILY};
+ }}
+ QTextBrowser a {{ color: {get_surface_color("text_secondary")}; text-decoration: none; }}
""")
# Style the run button with a contrasting text color based on background brightness.
@@ -280,9 +281,9 @@ def _setup_ui(self):
border-color: {button_colors["border"].darker(105).name()};
}}
QPushButton:disabled {{
- background-color: #2B2B2B;
- border-color: #353535;
- color: #7B7B7B;
+ background-color: {get_surface_color("field")};
+ border-color: {get_surface_color("border")};
+ color: {get_surface_color("text_muted")};
}}
""")
@@ -480,16 +481,16 @@ def paint(self, painter, option, widget=None):
path = QPainterPath()
path.addRoundedRect(0, 0, self.width, self.height, 10, 10)
- painter.setBrush(QColor("#2D2D2D"))
-
+ painter.setBrush(QColor(get_surface_color("field")))
+
web_color = node_colors["border"]
pen = QPen(web_color, 1.5)
if self.isSelected():
pen = QPen(palette.SELECTION, 2)
elif self.hovered:
- pen = QPen(QColor("#FFFFFF"), 2)
-
+ pen = QPen(QColor(get_surface_color("text_bright")), 2)
+
painter.setPen(pen)
painter.drawPath(path)
@@ -526,7 +527,7 @@ def paint(self, painter, option, widget=None):
return
if self.is_collapsed:
- painter.setPen(QColor("#FFFFFF"))
+ painter.setPen(QColor(get_surface_color("text_bright")))
font = canvas_font(self.scene(), weight=QFont.Weight.Bold)
painter.setFont(font)
painter.drawText(QRectF(40, 0, self.width - 80, self.height), Qt.AlignmentFlag.AlignVCenter, "Web Search")
@@ -535,7 +536,7 @@ def paint(self, painter, option, widget=None):
icon.paint(painter, QRect(10, 10, 20, 20))
self.collapse_button_rect = QRectF(self.width - 35, 5, 30, 30)
- expand_icon = qta.icon('fa5s.expand-arrows-alt', color='#FFFFFF' if self.hovered else '#888888')
+ expand_icon = qta.icon('fa5s.expand-arrows-alt', color=get_surface_color("text_bright") if self.hovered else get_surface_color("chrome_inactive"))
expand_icon.paint(painter, QRect(int(self.width - 30), 10, 20, 20))
else:
if self.hovered:
@@ -544,7 +545,7 @@ def paint(self, painter, option, widget=None):
painter.setPen(QColor(255, 255, 255, 150))
painter.drawRoundedRect(self.collapse_button_rect.adjusted(6,6,-6,-6), 4, 4)
- icon_pen = QPen(QColor("#FFFFFF"), 2)
+ icon_pen = QPen(QColor(get_surface_color("text_bright")), 2)
painter.setPen(icon_pen)
center = self.collapse_button_rect.center()
painter.drawLine(int(center.x() - 4), int(center.y()), int(center.x() + 4), int(center.y()))
diff --git a/graphlink_app/graphlink_widgets/overlays.py b/graphlink_app/graphlink_widgets/overlays.py
index 6dda58e..4d38d56 100644
--- a/graphlink_app/graphlink_widgets/overlays.py
+++ b/graphlink_app/graphlink_widgets/overlays.py
@@ -4,7 +4,8 @@
from PySide6.QtCore import QPointF, Property, QEasingCurve, QParallelAnimationGroup, QPropertyAnimation, QRectF, Qt, Signal
from PySide6.QtGui import QColor, QBrush, QFont, QFontMetrics, QKeySequence, QLinearGradient, QPainter, QPainterPath, QPen, QShortcut
from PySide6.QtWidgets import QGraphicsObject, QHBoxLayout, QLabel, QLineEdit, QPushButton, QWidget
-from graphlink_config import get_current_palette, get_semantic_color, is_monochrome_theme
+from graphlink_config import get_current_palette, get_graph_node_colors, get_semantic_color, get_surface_color, is_monochrome_theme
+from graphlink_styles import FONT_FAMILY_NAME
from .loading_visuals import paint_orbital_loading_spinner
class LoadingAnimation(QGraphicsObject):
@@ -136,19 +137,19 @@ def _surface_colors(self):
accent = QColor(palette.AI_NODE)
monochrome = is_monochrome_theme()
- body_start = self._mix_color(QColor("#282828"), accent, 0.04 if not monochrome else 0.02)
+ body_start = self._mix_color(QColor(get_surface_color("field")), accent, 0.04 if not monochrome else 0.02)
body_start.setAlpha(210)
- body_end = self._mix_color(QColor("#1A1A1A"), accent, 0.02 if not monochrome else 0.01)
+ body_end = self._mix_color(QColor(get_surface_color("window")), accent, 0.02 if not monochrome else 0.01)
body_end.setAlpha(192)
- header_start = self._mix_color(QColor("#323232"), accent, 0.30 if not monochrome else 0.08)
+ header_start = self._mix_color(get_graph_node_colors()["header_end"], accent, 0.30 if not monochrome else 0.08)
header_start.setAlpha(188)
- header_end = self._mix_color(QColor("#1C1C1C"), accent, 0.18 if not monochrome else 0.04)
+ header_end = self._mix_color(QColor(get_surface_color("window")), accent, 0.18 if not monochrome else 0.04)
header_end.setAlpha(170)
- badge_fill = self._mix_color(QColor("#2B2B2B"), accent, 0.58 if not monochrome else 0.12)
+ badge_fill = self._mix_color(QColor(get_surface_color("field")), accent, 0.58 if not monochrome else 0.12)
badge_fill.setAlpha(138)
- descriptor_text = self._mix_color(QColor("#A5A5A5"), accent, 0.14 if not monochrome else 0.04)
+ descriptor_text = self._mix_color(QColor(get_surface_color("text_label")), accent, 0.14 if not monochrome else 0.04)
descriptor_text.setAlpha(175)
- content_panel_border = self._mix_color(QColor("#393939"), accent, 0.08 if not monochrome else 0.03)
+ content_panel_border = self._mix_color(QColor(get_surface_color("border")), accent, 0.08 if not monochrome else 0.03)
content_panel_border.setAlpha(135)
return {
@@ -158,7 +159,7 @@ def _surface_colors(self):
"header_start": header_start,
"header_end": header_end,
"badge_fill": badge_fill,
- "badge_text": QColor("#F6F6F6"),
+ "badge_text": QColor(get_surface_color("text_bright")),
"descriptor_text": descriptor_text,
"content_panel_fill": QColor(17, 20, 23, 182),
"content_panel_border": content_panel_border,
@@ -237,7 +238,7 @@ def paint(self, painter, option, widget):
QPointF(self.width - 10, self.HEADER_HEIGHT),
)
- badge_font = QFont("Segoe UI", 8, QFont.Weight.DemiBold)
+ badge_font = QFont(FONT_FAMILY_NAME, 8, QFont.Weight.DemiBold)
painter.setFont(badge_font)
badge_text = "Assistant"
badge_metrics = QFontMetrics(badge_font)
@@ -254,7 +255,7 @@ def paint(self, painter, option, widget):
painter.setPen(badge_text_color)
painter.drawText(badge_rect, Qt.AlignmentFlag.AlignCenter, badge_text)
- descriptor_font = QFont("Segoe UI", 8)
+ descriptor_font = QFont(FONT_FAMILY_NAME, 8)
painter.setFont(descriptor_font)
painter.setPen(colors["descriptor_text"])
painter.drawText(
@@ -273,9 +274,9 @@ def paint(self, painter, option, widget):
text_left = content_rect.left() + 68
text_width = max(80.0, content_rect.width() - 84.0)
- title_font = QFont("Segoe UI", 10, QFont.Weight.DemiBold)
+ title_font = QFont(FONT_FAMILY_NAME, 10, QFont.Weight.DemiBold)
painter.setFont(title_font)
- title_color = QColor("#F5F5F5")
+ title_color = QColor(get_surface_color("text_bright"))
title_color.setAlpha(220)
painter.setPen(title_color)
painter.drawText(
@@ -285,9 +286,9 @@ def paint(self, painter, option, widget):
)
if self.subtitle:
- subtitle_font = QFont("Segoe UI", 8)
+ subtitle_font = QFont(FONT_FAMILY_NAME, 8)
painter.setFont(subtitle_font)
- subtitle_color = QColor("#B6B6B6")
+ subtitle_color = QColor(get_surface_color("text_secondary"))
subtitle_color.setAlpha(165)
painter.setPen(subtitle_color)
painter.drawText(
diff --git a/graphlink_app/graphlink_widgets/scrolling.py b/graphlink_app/graphlink_widgets/scrolling.py
index 3edfa36..70b0df6 100644
--- a/graphlink_app/graphlink_widgets/scrolling.py
+++ b/graphlink_app/graphlink_widgets/scrolling.py
@@ -3,6 +3,7 @@
from PySide6.QtCore import QRectF, Qt, Signal
from PySide6.QtGui import QBrush, QColor, QPainter
from PySide6.QtWidgets import QGraphicsObject, QWidget
+from graphlink_config import get_surface_color
class CustomScrollBar(QWidget):
valueChanged = Signal(float)
@@ -43,7 +44,7 @@ def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
- track_color = QColor("#2A2A2A")
+ track_color = QColor(get_surface_color("field"))
track_color.setAlpha(100)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(track_color)
@@ -76,7 +77,7 @@ def paintEvent(self, event):
else:
handle_position = 0
- handle_color = QColor("#6a6a6a") if self.hover else QColor("#555555")
+ handle_color = QColor(get_surface_color("handle_hover")) if self.hover else QColor(get_surface_color("handle"))
painter.setBrush(handle_color)
if self.orientation == Qt.Orientation.Vertical:
@@ -137,7 +138,7 @@ def boundingRect(self):
def paint(self, painter, option, widget=None):
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
- color = QColor("#6a6a6a") if self.hover else QColor("#555555")
+ color = QColor(get_surface_color("handle_hover")) if self.hover else QColor(get_surface_color("handle"))
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QBrush(color))
@@ -171,7 +172,7 @@ def boundingRect(self):
def paint(self, painter, option, widget=None):
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
- track_color = QColor("#2A2A2A")
+ track_color = QColor(get_surface_color("field"))
track_color.setAlpha(100)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QBrush(track_color))
diff --git a/graphlink_app/graphlink_widgets/splash.py b/graphlink_app/graphlink_widgets/splash.py
index 2fce05d..952ba92 100644
--- a/graphlink_app/graphlink_widgets/splash.py
+++ b/graphlink_app/graphlink_widgets/splash.py
@@ -3,7 +3,8 @@
from PySide6.QtCore import QPointF, Property, QEasingCurve, QParallelAnimationGroup, QPropertyAnimation, Qt, QTimer
from PySide6.QtGui import QColor, QFont, QFontMetrics, QGuiApplication, QPainter, QPainterPath, QPen
from PySide6.QtWidgets import QGraphicsDropShadowEffect, QLabel, QVBoxLayout, QWidget
-from graphlink_config import get_current_palette
+from graphlink_config import get_current_palette, get_surface_color
+from graphlink_styles import FONT_FAMILY_NAME
from graphlink_version import APP_VERSION
from .loading_visuals import paint_orbital_loading_spinner
@@ -99,7 +100,7 @@ def paintEvent(self, event):
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
- font = QFont("Segoe UI", 34, QFont.Weight.Bold)
+ font = QFont(FONT_FAMILY_NAME, 34, QFont.Weight.Bold)
font.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 2)
painter.setFont(font)
metrics = QFontMetrics(font)
@@ -131,8 +132,8 @@ def paintEvent(self, event):
y_offset = (1.0 - ease_p) * 25.0
- if i < 5:
- color = QColor("#FFFFFF")
+ if i < 5:
+ color = QColor(get_surface_color("text_bright"))
else:
color = QColor(palette.SELECTION)
@@ -177,14 +178,14 @@ def __init__(self, main_window):
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
content_layout.addWidget(self.status_label)
- self.setStyleSheet("""
- QWidget#splashContainer {
- background-color: #1E1E1E;
+ self.setStyleSheet(f"""
+ QWidget#splashContainer {{
+ background-color: {get_surface_color("window")};
border-radius: 8px;
- }
- QLabel {
- color: #747474;
- }
+ }}
+ QLabel {{
+ color: {get_surface_color("text_muted")};
+ }}
""")
screen = QGuiApplication.primaryScreen().geometry()
diff --git a/graphlink_app/tests/test_css_custom_properties.py b/graphlink_app/tests/test_css_custom_properties.py
index a57296c..c63724f 100644
--- a/graphlink_app/tests/test_css_custom_properties.py
+++ b/graphlink_app/tests/test_css_custom_properties.py
@@ -53,9 +53,20 @@ def test_every_value_is_a_flat_hex_color_except_font_family_and_alpha_groups(
f"--gl-composer-{key.replace('_', '-')}"
for key in gs.THEME_TOKENS[theme_name]["composer_alpha"]
}
+ # UI-refactor P0: structure tokens are px/ms/shadow/easing values,
+ # not colors - carved out by their real group names, mirroring the
+ # alpha carve-out's enumerate-don't-pattern-match approach.
+ structure_keys = {
+ f"--gl-{group}-{key}"
+ for group, entries in gs.STRUCTURE_TOKENS.items()
+ for key in entries
+ }
for key, value in props.items():
if key == "--gl-font-family":
continue
+ if key in structure_keys:
+ assert value, f"{key} is empty"
+ continue
if key in alpha_keys:
assert RGBA_RE.match(value), f"{key} = {value!r} is not an rgba() literal"
continue
@@ -114,11 +125,17 @@ def test_font_family_matches_the_shared_constant(self):
@pytest.mark.parametrize("theme_name", THEMES)
def test_total_property_count_has_no_missing_or_extra_keys(self, theme_name):
tokens = gs.THEME_TOKENS[theme_name]
- expected_group_count = sum(len(tokens[g]) for g in ("palette", "semantic", "neutral_button", "graph_node"))
+ # "surface"/"syntax" joined the included groups in UI-refactor P0.
+ expected_group_count = sum(
+ len(tokens[g])
+ for g in ("palette", "semantic", "neutral_button", "graph_node", "surface", "syntax")
+ )
expected_island_count = sum(len(tokens[g]) for g in gs._ISLAND_GROUPS)
expected_frame_count = len({name.removesuffix(" Header") for name in gs._FRAME_COLORS_BY_THEME[theme_name]})
+ expected_structure_count = sum(len(entries) for entries in gs.STRUCTURE_TOKENS.values())
expected_total = (
- expected_group_count + expected_island_count + expected_frame_count + 1
+ expected_group_count + expected_island_count + expected_frame_count
+ + expected_structure_count + 1
) # +1 for --gl-font-family
assert len(gs.css_custom_properties(theme_name)) == expected_total
diff --git a/graphlink_app/tests/test_tailwind_theme_css.py b/graphlink_app/tests/test_tailwind_theme_css.py
index 36e5188..e75e90c 100644
--- a/graphlink_app/tests/test_tailwind_theme_css.py
+++ b/graphlink_app/tests/test_tailwind_theme_css.py
@@ -50,14 +50,36 @@ def test_declares_exactly_one_theme_block(self):
def test_every_declaration_references_the_matching_gl_custom_property(self):
content = _read_generated_file()
- declarations = re.findall(r"--([a-z-]+):\s*var\((--gl-[a-z-]+)\);", content)
+ declarations = re.findall(r"--([a-z0-9-]+):\s*var\((--gl-[a-z0-9-]+)\);", content)
assert len(declarations) > 0
+ # UI-refactor P0: structure tokens route to Tailwind v4's proper
+ # non-color namespaces (spacing/radius/text/font-weight/shadow/
+ # duration/ease); everything else stays "color-gl-..." except the
+ # original "font-gl" special case. Mirrors tailwind_theme_css()'s
+ # own routing table.
+ structure_routes = {
+ "--gl-space-": "spacing-gl-",
+ "--gl-radius-": "radius-gl-",
+ "--gl-text-": "text-gl-",
+ "--gl-weight-": "font-weight-gl-",
+ "--gl-shadow-": "shadow-gl-",
+ }
for tailwind_name, gl_var in declarations:
- # Every Tailwind theme key is either "color-gl-..." (mirroring
- # the referenced --gl-... name exactly) or the one special-cased
- # "font-gl" for --gl-font-family.
if tailwind_name == "font-gl":
assert gl_var == "--gl-font-family"
+ continue
+ if gl_var == "--gl-motion-ease":
+ assert tailwind_name == "ease-gl"
+ continue
+ if gl_var.startswith("--gl-motion-"):
+ assert tailwind_name == f"duration-gl-{gl_var[len('--gl-motion-'):]}"
+ continue
+ for gl_prefix, tw_prefix in structure_routes.items():
+ if gl_var.startswith(gl_prefix):
+ assert tailwind_name == f"{tw_prefix}{gl_var[len(gl_prefix):]}", (
+ f"--{tailwind_name} does not mirror {gl_var} as expected"
+ )
+ break
else:
assert tailwind_name == f"color-{gl_var[len('--'):]}", (
f"--{tailwind_name} does not mirror {gl_var} as expected"
diff --git a/graphlink_app/tests/test_theme_tokens.py b/graphlink_app/tests/test_theme_tokens.py
index d9287fe..f649f72 100644
--- a/graphlink_app/tests/test_theme_tokens.py
+++ b/graphlink_app/tests/test_theme_tokens.py
@@ -86,9 +86,15 @@ def test_every_theme_has_all_expected_token_groups(self):
# "all four token groups" before that change landed.
# "composer"/"composer_alpha" were added by the composer token
# retrofit (see tests/test_composer_token_retrofit.py).
+ # "surface" was added by UI-refactor P0 (doc/UI_QA_AUDIT.md section
+ # 7): the clean neutral role set the hex-literal sweep migrated
+ # native painting/stylesheet code onto, exported to islands like
+ # palette/semantic (see test_ui_token_acceptance.py for the gate).
+ # "syntax" (same increment, sweep adjudication): PythonHighlighter's
+ # palette relocated from graphlink_pycoder.py.
expected = {
- "palette", "semantic", "neutral_button", "graph_node", "qss", "qss_alpha",
- "composer", "composer_alpha",
+ "palette", "semantic", "neutral_button", "graph_node", "surface",
+ "syntax", "qss", "qss_alpha", "composer", "composer_alpha",
}
for name in ("dark", "mono", "muted"):
tokens = gs.THEME_TOKENS[name]
diff --git a/graphlink_app/tests/test_ui_token_acceptance.py b/graphlink_app/tests/test_ui_token_acceptance.py
new file mode 100644
index 0000000..eb30bd1
--- /dev/null
+++ b/graphlink_app/tests/test_ui_token_acceptance.py
@@ -0,0 +1,184 @@
+"""UI-refactor P0 acceptance gate (doc/UI_QA_AUDIT.md section 7, P0).
+
+The audit's P0 acceptance criterion, codified so it can never silently
+regress: outside the token module (graphlink_styles.py), no UI source file
+may contain a hardcoded 6-digit hex color literal or a string-pasted
+"Segoe UI". Colors come from get_surface_color()/get_semantic_color()/
+get_current_palette()/THEME_TOKENS lookups; the font family comes from
+FONT_FAMILY (QSS stacks) / FONT_FAMILY_NAME (QFont).
+
+ALLOWLIST POLICY: entries in _HEX_ALLOWLIST are (relative path, reason)
+pairs for files whose remaining literals were adjudicated as domain DATA
+rather than UI chrome (e.g. syntax-highlight palettes pending their own
+token group). Every entry must carry a reason; an empty allowlist is the
+goal state. Adding an entry to dodge the gate defeats P0 - route new
+colors through THEME_TOKENS instead.
+
+Structure-token coverage lives here too: the P0 scales exist, are complete,
+and respect their own rules (4px spacing grid, no type size under 12px,
+three elevation levels, 150/200ms motion).
+"""
+
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+import graphlink_styles as gs
+
+APP_DIR = Path(__file__).resolve().parents[1]
+
+# The one module allowed to define color literals and the font family.
+_TOKEN_MODULES = {"graphlink_styles.py"}
+
+# Adjudicated exceptions: (path relative to graphlink_app/, reason). Each
+# entry was individually adjudicated during the P0 sweep (2026-07-22) as
+# domain DATA rather than UI chrome. Shrink this list, never grow it
+# casually - route new colors through THEME_TOKENS instead.
+_HEX_ALLOWLIST: dict[str, str] = {
+ "graphlink_context_menu.py": (
+ "per-theme QMenu state table carrying literals for NON-current themes "
+ "(get_surface_color resolves only the current theme); the menu system "
+ "is rebuilt wholesale in refactor phase P8, which retires this file's "
+ "styling entirely"
+ ),
+ "graphlink_exporter.py": (
+ "light-styled CSS baked into exported standalone HTML/PDF documents - "
+ "deliberately NOT themed to the running app; theming exports to the "
+ "app's dark theme would corrupt the user's output artifacts"
+ ),
+ "graphlink_font_control_bridge.py": (
+ "FONT_COLOR_PRESETS: user-pickable canvas font color swatches - "
+ "stable data choices offered to the user, not app chrome"
+ ),
+ "graphlink_grid_control_bridge.py": (
+ "grid-color preset swatches - user-pickable data choices, stable "
+ "across themes by design"
+ ),
+ "graphlink_grid_view_settings.py": (
+ "DEFAULT_GRID_COLOR in the deliberately Qt-free plain-data settings "
+ "model - a persisted user-preference default, not chrome"
+ ),
+ "graphlink_plugins/common/combo.py": (
+ "single #656565 default-parameter accent in a reusable combo widget's "
+ "public API signature - callers pass real accents; changing the "
+ "default's identity is an API change deferred past P0"
+ ),
+ "graphlink_canvas/graphlink_canvas_chart_item.py": (
+ "single #868686 'slate' entry in the chart data-series color cycle - "
+ "series colors are data, not chrome (rule 2 of the sweep)"
+ ),
+ "graphlink_canvas/graphlink_canvas_container.py": (
+ "DEFAULT_CONTAINER_COLOR: the container's persisted user-facing body "
+ "color default (saved into scene JSON) - scene data must keep its "
+ "color across theme switches, like DEFAULT_GRID_COLOR"
+ ),
+}
+
+# Files allowed to carry the literal string "Segoe UI" outside the token
+# module, same adjudication bar.
+_FONT_ALLOWLIST: dict[str, str] = {
+ "graphlink_font_control_bridge.py": (
+ "FONT_FAMILIES: the user-pickable font list offers 'Segoe UI' and "
+ "'Segoe UI Variable' as CHOICES - data presented to the user, not a "
+ "hardcoded app default (defaults go through FONT_FAMILY_NAME)"
+ ),
+}
+
+_HEX_RE = re.compile(r"#[0-9A-Fa-f]{6}\b")
+
+
+def _ui_source_files():
+ for path in sorted(APP_DIR.rglob("*.py")):
+ rel = path.relative_to(APP_DIR).as_posix()
+ if rel.startswith("tests/"):
+ continue
+ if path.name in _TOKEN_MODULES:
+ continue
+ yield rel, path
+
+
+class TestNoHardcodedHexOutsideTokenModules:
+ def test_no_ui_file_contains_a_hex_literal(self):
+ offenders = {}
+ for rel, path in _ui_source_files():
+ if rel in _HEX_ALLOWLIST:
+ continue
+ text = path.read_text(encoding="utf-8")
+ hits = _HEX_RE.findall(text)
+ if hits:
+ offenders[rel] = sorted(set(hits))
+ assert not offenders, (
+ "Hardcoded hex color literal(s) outside the token module - route "
+ f"them through THEME_TOKENS/get_surface_color instead:\n{offenders}"
+ )
+
+ def test_allowlist_entries_all_carry_reasons_and_still_exist(self):
+ for rel, reason in _HEX_ALLOWLIST.items():
+ assert reason.strip(), f"allowlist entry {rel} has no reason"
+ assert (APP_DIR / rel).is_file(), f"allowlist entry {rel} no longer exists - prune it"
+
+
+class TestNoHardcodedFontFamilyOutsideTokenModules:
+ def test_no_ui_file_string_pastes_segoe_ui(self):
+ offenders = []
+ for rel, path in _ui_source_files():
+ if rel in _FONT_ALLOWLIST:
+ continue
+ text = path.read_text(encoding="utf-8")
+ if "Segoe UI" in text:
+ offenders.append(rel)
+ assert not offenders, (
+ "String-pasted 'Segoe UI' outside the token module - use "
+ f"FONT_FAMILY / FONT_FAMILY_NAME from graphlink_styles:\n{offenders}"
+ )
+
+ def test_font_allowlist_entries_all_carry_reasons_and_still_exist(self):
+ for rel, reason in _FONT_ALLOWLIST.items():
+ assert reason.strip(), f"font allowlist entry {rel} has no reason"
+ assert (APP_DIR / rel).is_file(), f"font allowlist entry {rel} no longer exists - prune it"
+
+
+class TestStructureTokenScales:
+ def test_all_expected_groups_exist(self):
+ assert set(gs.STRUCTURE_TOKENS) == {
+ "space", "radius", "text", "weight", "shadow", "motion",
+ }
+
+ def test_spacing_is_a_4px_grid(self):
+ for key, px in gs.SPACE_PX.items():
+ assert px % 4 == 0, f"space-{key}={px}px breaks the 4px grid"
+ assert gs.SPACE_PX[1] == 4
+
+ def test_no_type_size_under_12px(self):
+ # Audit finding D2: 9-10px microtext. The ramp's floor is 12px.
+ for key, px in gs.TEXT_PX.items():
+ assert px >= 12, f"text-{key}={px}px is below the 12px floor"
+
+ def test_three_radius_steps(self):
+ assert set(gs.RADIUS_PX) == {"sm", "md", "lg"}
+ assert gs.RADIUS_PX["sm"] < gs.RADIUS_PX["md"] < gs.RADIUS_PX["lg"]
+
+ def test_three_elevation_levels_in_both_representations(self):
+ assert set(gs.STRUCTURE_TOKENS["shadow"]) == {"1", "2", "3"}
+ assert set(gs.ELEVATION_PARAMS) == {1, 2, 3}
+
+ def test_motion_scale(self):
+ motion = gs.STRUCTURE_TOKENS["motion"]
+ assert motion["fast"] == "150ms"
+ assert motion["base"] == "200ms"
+ assert motion["ease"].startswith("cubic-bezier(")
+
+ def test_structure_tokens_are_exported_to_every_theme_identically(self):
+ for theme in ("dark", "mono", "muted"):
+ props = gs.css_custom_properties(theme)
+ for group, entries in gs.STRUCTURE_TOKENS.items():
+ for key, value in entries.items():
+ assert props[f"--gl-{group}-{key}"] == value
+
+ def test_surface_group_exports_for_every_theme(self):
+ for theme in ("dark", "mono", "muted"):
+ props = gs.css_custom_properties(theme)
+ for key in gs.THEME_TOKENS[theme]["surface"]:
+ assert f"--gl-surface-{key.replace('_', '-')}" in props
diff --git a/web_ui/src/lib/tokens/gl-theme.css b/web_ui/src/lib/tokens/gl-theme.css
index ef5640c..1552601 100644
--- a/web_ui/src/lib/tokens/gl-theme.css
+++ b/web_ui/src/lib/tokens/gl-theme.css
@@ -19,6 +19,9 @@
--color-gl-graph-node-header-end: var(--gl-graph-node-header-end);
--color-gl-graph-node-header-start: var(--gl-graph-node-header-start);
--color-gl-graph-node-panel-fill: var(--gl-graph-node-panel-fill);
+ --duration-gl-base: var(--gl-motion-base);
+ --ease-gl: var(--gl-motion-ease);
+ --duration-gl-fast: var(--gl-motion-fast);
--color-gl-neutral-button-background: var(--gl-neutral-button-background);
--color-gl-neutral-button-border: var(--gl-neutral-button-border);
--color-gl-neutral-button-hover: var(--gl-neutral-button-hover);
@@ -29,6 +32,9 @@
--color-gl-palette-nav-highlight: var(--gl-palette-nav-highlight);
--color-gl-palette-selection: var(--gl-palette-selection);
--color-gl-palette-user-node: var(--gl-palette-user-node);
+ --radius-gl-lg: var(--gl-radius-lg);
+ --radius-gl-md: var(--gl-radius-md);
+ --radius-gl-sm: var(--gl-radius-sm);
--color-gl-semantic-artifact: var(--gl-semantic-artifact);
--color-gl-semantic-conversation-ai-bubble: var(--gl-semantic-conversation-ai-bubble);
--color-gl-semantic-conversation-user-bubble: var(--gl-semantic-conversation-user-bubble);
@@ -38,4 +44,45 @@
--color-gl-semantic-status-info: var(--gl-semantic-status-info);
--color-gl-semantic-status-success: var(--gl-semantic-status-success);
--color-gl-semantic-status-warning: var(--gl-semantic-status-warning);
+ --shadow-gl-1: var(--gl-shadow-1);
+ --shadow-gl-2: var(--gl-shadow-2);
+ --shadow-gl-3: var(--gl-shadow-3);
+ --spacing-gl-1: var(--gl-space-1);
+ --spacing-gl-2: var(--gl-space-2);
+ --spacing-gl-3: var(--gl-space-3);
+ --spacing-gl-4: var(--gl-space-4);
+ --spacing-gl-5: var(--gl-space-5);
+ --spacing-gl-6: var(--gl-space-6);
+ --spacing-gl-8: var(--gl-space-8);
+ --color-gl-surface-border: var(--gl-surface-border);
+ --color-gl-surface-border-strong: var(--gl-surface-border-strong);
+ --color-gl-surface-chrome-inactive: var(--gl-surface-chrome-inactive);
+ --color-gl-surface-divider: var(--gl-surface-divider);
+ --color-gl-surface-field: var(--gl-surface-field);
+ --color-gl-surface-handle: var(--gl-surface-handle);
+ --color-gl-surface-handle-hover: var(--gl-surface-handle-hover);
+ --color-gl-surface-inset: var(--gl-surface-inset);
+ --color-gl-surface-inset-deep: var(--gl-surface-inset-deep);
+ --color-gl-surface-node-body: var(--gl-surface-node-body);
+ --color-gl-surface-text-bright: var(--gl-surface-text-bright);
+ --color-gl-surface-text-label: var(--gl-surface-text-label);
+ --color-gl-surface-text-muted: var(--gl-surface-text-muted);
+ --color-gl-surface-text-primary: var(--gl-surface-text-primary);
+ --color-gl-surface-text-secondary: var(--gl-surface-text-secondary);
+ --color-gl-surface-text-soft: var(--gl-surface-text-soft);
+ --color-gl-surface-text-strong: var(--gl-surface-text-strong);
+ --color-gl-surface-window: var(--gl-surface-window);
+ --color-gl-syntax-builtin: var(--gl-syntax-builtin);
+ --color-gl-syntax-comment: var(--gl-syntax-comment);
+ --color-gl-syntax-function: var(--gl-syntax-function);
+ --color-gl-syntax-keyword: var(--gl-syntax-keyword);
+ --color-gl-syntax-number: var(--gl-syntax-number);
+ --color-gl-syntax-string: var(--gl-syntax-string);
+ --text-gl-base: var(--gl-text-base);
+ --text-gl-lg: var(--gl-text-lg);
+ --text-gl-sm: var(--gl-text-sm);
+ --text-gl-xl: var(--gl-text-xl);
+ --text-gl-xs: var(--gl-text-xs);
+ --font-weight-gl-regular: var(--gl-weight-regular);
+ --font-weight-gl-semibold: var(--gl-weight-semibold);
}
diff --git a/web_ui/src/lib/tokens/gl-vars-dev.css b/web_ui/src/lib/tokens/gl-vars-dev.css
index a55183a..f2328d8 100644
--- a/web_ui/src/lib/tokens/gl-vars-dev.css
+++ b/web_ui/src/lib/tokens/gl-vars-dev.css
@@ -75,6 +75,9 @@
--gl-graph-node-header-end: #333333;
--gl-graph-node-header-start: #3c3c3c;
--gl-graph-node-panel-fill: #202020;
+ --gl-motion-base: 200ms;
+ --gl-motion-ease: cubic-bezier(0.2, 0, 0, 1);
+ --gl-motion-fast: 150ms;
--gl-neutral-button-background: #393939;
--gl-neutral-button-border: #585858;
--gl-neutral-button-hover: #484848;
@@ -85,6 +88,9 @@
--gl-palette-nav-highlight: #949494;
--gl-palette-selection: #858585;
--gl-palette-user-node: #838383;
+ --gl-radius-lg: 12px;
+ --gl-radius-md: 8px;
+ --gl-radius-sm: 4px;
--gl-semantic-artifact: #828282;
--gl-semantic-conversation-ai-bubble: #323232;
--gl-semantic-conversation-user-bubble: #696969;
@@ -94,4 +100,45 @@
--gl-semantic-status-info: #828282;
--gl-semantic-status-success: #838383;
--gl-semantic-status-warning: #919191;
+ --gl-shadow-1: 0 1px 3px rgba(0, 0, 0, 0.40);
+ --gl-shadow-2: 0 4px 12px rgba(0, 0, 0, 0.45);
+ --gl-shadow-3: 0 8px 28px rgba(0, 0, 0, 0.55);
+ --gl-space-1: 4px;
+ --gl-space-2: 8px;
+ --gl-space-3: 12px;
+ --gl-space-4: 16px;
+ --gl-space-5: 20px;
+ --gl-space-6: 24px;
+ --gl-space-8: 32px;
+ --gl-surface-border: #3F3F3F;
+ --gl-surface-border-strong: #505050;
+ --gl-surface-chrome-inactive: #888888;
+ --gl-surface-divider: #424242;
+ --gl-surface-field: #272727;
+ --gl-surface-handle: #555555;
+ --gl-surface-handle-hover: #6A6A6A;
+ --gl-surface-inset: #202020;
+ --gl-surface-inset-deep: #121212;
+ --gl-surface-node-body: #252525;
+ --gl-surface-text-bright: #FFFFFF;
+ --gl-surface-text-label: #A4A4A4;
+ --gl-surface-text-muted: #767676;
+ --gl-surface-text-primary: #E0E0E0;
+ --gl-surface-text-secondary: #BDBDBD;
+ --gl-surface-text-soft: #CCCCCC;
+ --gl-surface-text-strong: #F1F1F1;
+ --gl-surface-window: #1E1E1E;
+ --gl-syntax-builtin: #A2A2A2;
+ --gl-syntax-comment: #626262;
+ --gl-syntax-function: #A3A3A3;
+ --gl-syntax-keyword: #909090;
+ --gl-syntax-number: #A2A2A2;
+ --gl-syntax-string: #B5B5B5;
+ --gl-text-base: 14px;
+ --gl-text-lg: 16px;
+ --gl-text-sm: 13px;
+ --gl-text-xl: 20px;
+ --gl-text-xs: 12px;
+ --gl-weight-regular: 400;
+ --gl-weight-semibold: 600;
}