Files
f9f6bc01fd
CI / test (push) Canceled after 0s
Feature/delta team/epic r04 (#7)
## Summary

epic r04 - begin refactor

## Change Type

- [x] Cowork feature
- [ ] Bug fix
- [ ] Core AI contribution
- [ ] Test / hardening
- [ ] Performance
- [ ] Documentation

## Related Work

Cowork Task:

Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets

Core AI Issue:

Core Task:

Related PR:

## Scope

What is intentionally included?

What is intentionally NOT included?

## Validation

- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual verification
- [ ] Regression check

Commands / evidence:

## Security Impact

Permission / credential / network / customer data impact:

## Compatibility

- [ ] No breaking change
- [ ] Breaking change documented

## Reviewer Notes

Anything Cowork reviewers should pay attention to.

---------

Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com>
Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com>
Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com>
Co-authored-by: Vu Dam Tuan <vudt15@fpt.com>
Co-authored-by: Hiep Ha Van <hiephv3@fpt.com>
Co-authored-by: Lam Hoang Van <lamhv7@fpt.com>
Reviewed-on: #7
Co-authored-by: Duy Le Huu <duylh19@fpt.com>
2026-08-31 05:15:13 +00:00

131 lines
4.5 KiB
Python

"""The app's visual system: semantic design tokens + one style sheet template.
This replaces the previous mechanism, which was two hand-written Qt style
sheets (``_DARK`` / ``_LIGHT``) that duplicated each other and hard-coded ~105
hex literals, with a single source of truth:
Palette (tokens) -> _TEMPLATE (one QSS) -> stylesheet(theme)
Rules of the system
-------------------
* **Nothing outside this module names a colour.** Widgets that paint with
``QPainter`` (charts, canvases, syntax highlighters) call :func:`palette` and
read a token. Widgets that style themselves declaratively should instead be
given an ``objectName`` and styled in ``_TEMPLATE`` below.
* **Tokens are semantic, not literal.** ``danger``/``text_muted``/``code_string``
— never ``blue``/``grey2``. Adding a theme means adding a :class:`Palette`,
not editing a style sheet.
* **No gradients, no glows.** The palette is Visual Studio Code's — "Dark
Modern" and "Light Modern", taken from the shipped theme JSON. Flat surfaces,
square-ish corners, one accent spent only on what the user acts on. Depth
comes from the surface ramp and hairline borders, not from colour. Note the
VS Code silhouette: the nav rail is *darker* than the content area, not
lighter.
Contrast is held to WCAG AA (4.5:1) for body text and for text on filled
buttons. That is why ``accent`` and ``accent_solid`` are separate tokens: on a
dark background a blue readable *as text* is too light to carry white *as a
fill*, so each role gets the tint that passes.
Four VS Code values fall below AA and are nudged just far enough to clear it —
dark line numbers (3.59:1), light faint text on the sidebar (4.28:1), light
green (4.33:1) and light amber (3.12:1). Each carries a comment naming the
original value, so the deviation is auditable rather than silent.
"""
from __future__ import annotations
from .palettes import ( # noqa: F401 — giữ đường vào cũ
DARK, LIGHT, Palette, _chevron_asset, _FONT, _MONO, _PALETTES,
)
from .qss import _TEMPLATE
from dataclasses import dataclass, asdict
from string import Template
# ---------------------------------------------------------------------------
# The one style sheet. `$token` placeholders are filled from the Palette above;
# use `${token}px` where a unit follows the name.
#
# Read it as a cascade: reset -> shell -> surfaces -> controls -> chrome.
# ---------------------------------------------------------------------------
def resolve_theme(theme: str) -> str:
"""Resolve ``'system'`` to ``'dark'``/``'light'`` from the OS colour scheme."""
if theme in _PALETTES:
return theme
try:
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication
app = QApplication.instance()
if app is not None:
scheme = app.styleHints().colorScheme()
return "light" if scheme == Qt.ColorScheme.Light else "dark"
except Exception:
pass
return "dark"
def palette(theme: str) -> Palette:
"""The token set for ``theme``. Painting code reads its colours from here."""
return _PALETTES[resolve_theme(theme)]
# The theme the running app is currently showing. Painting code (paintEvent,
# QSyntaxHighlighter, canvas items) reads it via current_palette() instead of
# re-reading config.json — that used to cost a file open per repaint.
_active_theme = "dark"
def set_active_theme(theme: str) -> str:
"""Record the theme the app just applied. Call this next to every
``QApplication.setStyleSheet(stylesheet(...))``. Returns the resolved name."""
global _active_theme
_active_theme = resolve_theme(theme)
return _active_theme
def current_theme() -> str:
"""The resolved theme ('dark'/'light') the app is showing right now."""
return _active_theme
def current_palette() -> Palette:
"""Tokens for the theme the app is showing right now."""
return _PALETTES[_active_theme]
def stylesheet(theme: str) -> str:
"""The application-wide Qt style sheet for ``theme``."""
p = palette(theme)
values = asdict(p)
values["chevron_down"] = _chevron_asset("down", p.text_muted)
values["chevron_up"] = _chevron_asset("up", p.text_muted)
return _TEMPLATE.substitute(values)
def role_colors(theme: str) -> dict[str, str]:
"""Conversation/graph node colours keyed by role."""
p = palette(theme)
return {
"user": p.role_user,
"assistant": p.role_assistant,
"tool": p.role_tool,
"result": p.role_result,
"error": p.role_error,
}