"""Central theme tokens — every colour Annie uses lives here.
Two distinct colour systems are kept apart on purpose:
* **UI status colours** (:data:`STATUS_COLORS`) — chrome that conveys a row's
link state in the Dataset summary and Browse list. Referenced by name, never as
raw hex inline.
* **Box-drawing colours** (:func:`color_for_track` and friends, in
:mod:`annie.color`) — colours burned onto the video frames themselves, given in
BGR to match the render convention.
Keeping both here means a redesign touches one file.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Literal
if TYPE_CHECKING:
from annie.core.models import RowStatus
PRIMARY = "#006d77"
"""App-wide primary/accent colour, applied via NiceGUI's global colour call."""
ACCENT = "#22c55e"
"""More vivid green, e.g. the "validated" check."""
LOGO_MARK_SVG = (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" '
'style="width:100%;height:100%">'
'<g fill="none" stroke="#cbd5e1" stroke-width="6" '
'stroke-linecap="round" stroke-linejoin="round">'
'<path d="M14 22 L14 12 L24 12"/><path d="M40 12 L50 12 L50 22"/>'
'<path d="M14 42 L14 52 L24 52"/><path d="M40 52 L50 52 L50 42"/></g>'
'<path d="M21 33 L29 41 L42 24" fill="none" stroke="#22c55e" stroke-width="7" '
'stroke-linecap="round" stroke-linejoin="round"/></svg>'
)
"""The brand mark — a detection-box bracket enclosing a green validation check.
Matches ``docs/assets/logo.svg`` (mark only, no wordmark) and is used for the app
header and the browser-tab favicon. The SVG scales to its container.
"""
SUCCESS = "#2a9d8f"
"""Teal — linked / complete / usable."""
WARNING = "#f77f00"
"""Amber — video only / watchable, annotation off."""
DANGER = "#d90429"
"""Coral — annotation only / nothing to preview."""
NEUTRAL = "#676D78"
"""Grey — totals, secondary text."""
# ── Segment-review decision colours ───────────────────────────────────────────
# The accept/drop buttons and the decided-row border highlight. Kept together and
# muted (darker than the vivid status colours) so they read as a deliberate verdict;
# change these two to restyle the whole Segment-review decision affordance.
DECISION_ACCEPT = "#1b6b52"
"""Dark green — an accepted clip (button + row-border highlight)."""
DECISION_DROP = "#8b1e2d"
"""Dark red — a dropped clip (button + row-border highlight)."""
DECISION_UNDECIDED = "#676D78"
"""Grey — the "Undecided" reset button; a clip with no verdict carries no highlight."""
DECISION_ACCEPT_TINT = "#e8f3ef"
"""Pale green fill behind an accepted clip — the lighter inside of its green border."""
DECISION_DROP_TINT = "#f9eaec"
"""Pale red fill behind a dropped clip — the lighter inside of its red border."""
ROW_BORDER = "1px solid #d5d9df"
"""The light border every Browse/Annotator row card carries, so its extent is visible."""
# ── Event-annotation timeline colours ─────────────────────────────────────────
# The timeline is a CSS/SVG surface, so these are hex strings (unlike the BGR box
# palette in annie.color). One colour per track lane, cycled by lane order, so
# stacked tracks read apart at a glance; the playhead and selection are called out
# separately.
EVENT_TRACK_PALETTE: tuple[str, ...] = (
"#3a86ff", # blue
"#8338ec", # violet
"#fb5607", # orange
"#2a9d8f", # teal
"#e63946", # red
"#606c38", # olive
)
"""Per-lane fill colours for timeline event boxes, cycled by the lane's order."""
EVENT_PLAYHEAD = "#d90429"
"""Coral — the vertical playhead line marking the current time across all lanes."""
EVENT_SELECTED = "#111827"
"""Near-black — the 2px outline of the currently selected event box."""
EVENT_BORDER = "#444b54"
"""Dark grey — the 1px outline every event box carries, so a pale/pastel fill stays visible
against the white lane (a yellow box on white would otherwise disappear)."""
EVENT_PENDING = "#f59e0b"
"""Amber — the translucent band drawn from an event's start to the live playhead while the
reviewer is mid-capture (start set, end not yet), and the armed "Set end" button."""
[docs]
def event_track_color(ordinal: int) -> str:
"""Return the timeline fill colour for a track at vertical position ``ordinal``.
Args:
ordinal: The lane's zero-based position from the top.
Returns:
A hex colour from :data:`EVENT_TRACK_PALETTE`, cycled by ``ordinal``.
"""
return EVENT_TRACK_PALETTE[ordinal % len(EVENT_TRACK_PALETTE)]
VDET_COLOR = "#1d3557"
"""Navy blue — vdet-related metrics."""
TRACK_COLOR = "#40916c"
"""Pastel green — track-related metrics."""
AVAILABLE = "#2a9d8f"
"""Teal — the path exists and was read."""
UNAVAILABLE = "#d90429"
"""Coral — the configured path is missing."""
LABEL_COLOR = "#5a189a"
"""Violet — distinct from vdet/track/status chips."""
VIDEO_TAG_COLOR = "#3a86ff"
"""Bright blue — has video frames."""
AUDIO_TAG_COLOR = "#fb8500"
"""Orange — has an audio stream."""
HEADER_FG = "#ffffff"
"""White header text/icons, readable on the dark primary header bar."""
STATUS_COLORS: dict[RowStatus, str] = {
"linked": SUCCESS,
"video_only": WARNING,
"annotation_only": DANGER,
}
"""Status → colour, the single source of truth for the tri-state UI treatment."""
STATUS_LABELS: dict[RowStatus, str] = {
"linked": "linked",
"video_only": "video only",
"annotation_only": "annotation only",
}
"""Status → short human label shown on pills and summary cards."""
STATUS_ICONS: dict[RowStatus, str] = {
"linked": "link",
"video_only": "videocam",
"annotation_only": "file_present",
}
"""Status → Tabler icon name used on cards and pills."""
ColorName = Literal["success", "warning", "danger", "neutral", "primary", "accent"]
"""The semantic colour names accepted by :func:`color`."""
#: Semantic colour name → hex, backing :func:`color` lookups.
_BY_NAME: dict[str, str] = {
"success": SUCCESS,
"warning": WARNING,
"danger": DANGER,
"neutral": NEUTRAL,
"primary": PRIMARY,
"accent": ACCENT,
}
[docs]
def status_color(status: RowStatus) -> str:
"""Return the hex colour for a row status.
Args:
status: One of the :data:`~annie.models.RowStatus` values.
Returns:
The hex colour string for that status.
"""
return STATUS_COLORS[status]
[docs]
def color(name: ColorName) -> str:
"""Resolve a semantic colour name to its hex value.
Args:
name: A semantic colour name (e.g. ``"success"``).
Returns:
The hex colour string.
Raises:
KeyError: If ``name`` is not a known semantic colour.
"""
return _BY_NAME[name]