Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -9267,3 +9267,110 @@ body.sy-embed-hosting {
font-family: ui-monospace, Menlo, monospace;
font-size: 0.85em;
}

/* ── Agents / okstratr theme sync (light + dark) ───────────────────
* Observer CSS hard-codes Omarchy dark tokens on :root. Same-document
* embed inherits the host document, so under Switchbay light chrome
* --panel/--recessed/--chip stay dark while --bg/--text flip light
* (Switchbay :root[data-theme] wins on those shared names) → dark-on-
* light desk/DAG. Remap okstratr tokens on the proxied mount so both
* themes match SPA chrome. :root[data-theme] flips live with the
* footer toggle (no reload); no Attention / network_org_hooks here.
*/
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html {
--bg: #f8f9fb;
--panel: #ffffff;
--recessed: #f0f2f5;
--border: #dfe2e7;
--divider: #dfe2e7;
--text: #1a1d22;
--fg: #1a1d22;
--muted: #5a6068;
--accent: #2f9874; /* Switchbay light mint */
--ok: #3d8b4f;
--warn: #b8860b;
--bad: #c44;
--purple: #7c5cbf;
--chip: #e8ebf0;
--focus: #2f987433;
--focus-border: #2f9874;
color-scheme: light;
background: var(--bg) !important;
color: var(--text) !important;
}
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .top,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .panel,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .main,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .rail,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .config,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .desk-card,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .card,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .side,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .board {
border-color: var(--border) !important;
}
/* Neutral chips/buttons only — do not wipe .chip.running / .chip.idle status colours. */
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .chip:not(.running):not(.idle),
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .btn {
background: var(--chip) !important;
border-color: var(--border) !important;
color: var(--text) !important;
}
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .chip.running,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .chip.idle {
background: var(--chip) !important;
}
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .chip.running {
color: var(--ok) !important;
border-color: #3d5a3a !important;
}
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .chip.idle {
color: var(--warn) !important;
border-color: var(--border) !important;
}
/* Standing placeholder badge (.desk-obj--standing) uses idle amber, not --text/--muted. */
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .desk-obj--standing {
color: var(--warn) !important;
}
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .btn.primary,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .btn.mini.active,
:root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .kind-chip.active {
background: var(--focus) !important;
border-color: var(--accent) !important;
color: var(--fg) !important;
}

:root[data-theme="dark"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html {
/* Keep okstratr desk tokens coherent with Switchbay dark SPA (mint accent). */
--bg: #0f1115;
--panel: #161a21;
--recessed: #14181f;
--border: #232830;
--divider: #232830;
--text: #e6e8eb;
--fg: #e6e8eb;
--muted: #9aa0a8;
--accent: #6be8b3; /* Switchbay dark mint */
--ok: #9ece6a;
--warn: #e0af68;
--bad: #f7768e;
--purple: #bb9af7;
--chip: #1f242c;
--focus: #6be8b344;
--focus-border: #6be8b3;
color-scheme: dark;
background: var(--bg) !important;
color: var(--text) !important;
}

/* Dark: same standing badge amber. Do not inherit the mount's --text. */
:root[data-theme="dark"] .sy-proxied-skill[data-kind="okstratr"] .sy-proxied-skill-html .desk-obj--standing {
color: var(--warn) !important;
}

:root[data-theme="light"] .sy-agents,
:root[data-theme="light"] .sy-agents-section,
:root[data-theme="light"] .sy-agents-row {
background: var(--bg-modal, #fff);
color: var(--text, #1a1d22);
}
63 changes: 63 additions & 0 deletions tests/unit/test_agents_theme_sync_css.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Lock Agents/okstratr theme-sync CSS: Switchbay mint accents + status chips."""

from __future__ import annotations

from pathlib import Path

CSS = Path(__file__).resolve().parents[2] / "frontend" / "src" / "index.css"


def _okstratr_theme_block() -> str:
text = CSS.read_text(encoding="utf-8")
start = text.index("Agents / okstratr theme sync")
# Stop before the built-in Agents light block that follows.
end = text.index(':root[data-theme="light"] .sy-agents', start)
return text[start:end]


def test_accents_match_switchbay_mint():
block = _okstratr_theme_block()
assert "--accent: #2f9874" in block
assert "--accent: #6be8b3" in block
assert "#3d6fd9" not in block
assert "#7aa2f7" not in block


def test_light_chip_rule_preserves_running_and_idle():
block = _okstratr_theme_block()
assert ".chip:not(.running):not(.idle)" in block
assert ".chip.running" in block
assert "color: var(--ok)" in block
assert ".chip.idle" in block
assert "color: var(--warn)" in block


def _rule_body(block: str, selector_needle: str) -> str:
idx = block.index(selector_needle)
return block[idx:block.index("}", idx)]


def test_standing_badge_stays_warn_not_text():
"""Placeholder .desk-obj--standing is idle amber in light and dark.

A blanket color: var(--text) must not wash the standing badge gray.
"""
block = _okstratr_theme_block()
light = _rule_body(
block,
':root[data-theme="light"] .sy-proxied-skill[data-kind="okstratr"] '
".sy-proxied-skill-html .desk-obj--standing",
)
dark = _rule_body(
block,
':root[data-theme="dark"] .sy-proxied-skill[data-kind="okstratr"] '
".sy-proxied-skill-html .desk-obj--standing",
)
for rule in (light, dark):
assert "color: var(--warn)" in rule
assert "var(--text)" not in rule
assert "var(--muted)" not in rule
# Neutral chip wash still excludes status chips (idle amber / running green).
neutral = _rule_body(block, ".chip:not(.running):not(.idle)")
assert "desk-obj--standing" not in neutral
assert ".chip.idle" not in neutral.split("{", 1)[0]
Loading