"""Theming reference for spaday.
Theming is authored on any component with :meth:`~spaday.component.Component.css` (CSS custom
properties — the theme knobs), :meth:`~spaday.component.Component.style` (inline declarations), and
:meth:`~spaday.component.Component.classes` (variant/state classes). There is no separate theme object;
a custom property set on a container cascades, so an **app-level** theme is just ``.css(...)`` on the
``App`` root.
``SHELL_TOKENS`` documents the ``spa-*`` shell's own override tokens (the ``css()`` kwarg → the CSS
custom property it drives and what it controls). Component packages use :class:`Token` to include
the shell property a package token inherits. The shell ships neutral **light and dark** defaults —
the dark palette is keyed off WebAwesome's ``wa-dark`` class (with ``wa-light`` flipping a nested
island back), so ``App(...).bind_root_class("wa-dark", "dark")`` alone re-themes the whole page. Both
palettes are emitted at zero specificity, so an application or component package overrides them by
mapping its own theme onto these variables::
from spaday.components.shell import App
App().css(spa_surface="#111", spa_border="#333", spa_text="#eee") # retheme the whole shell
Component packages
------------------
Every token of every component is set the same way — ``css()`` takes **arbitrary** custom properties,
so a third-party design system's own tokens are authored from Python without a stylesheet::
App().css(wa_color_surface_default="#111") # --wa-color-surface-default
A spaday component package names its own tokens ``--spa-<package>-<thing>`` (see
:func:`package_token`) and chains each one to the shell token it belongs to, so three layers apply in
order — the package's token, then the shell's, then a literal that keeps a standalone page coherent::
fill: var(--spa-dagre-node-fill, var(--spa-surface-2, #fafafa));
An app therefore re-themes every package at once by setting the shell tokens, or re-themes one
package without touching the others by setting its ``--spa-<package>-*`` tokens. Each package
publishes a ``TOKENS`` mapping listing what it exposes. A :class:`Token` stores the property,
description, and optional shell fallback in one record while remaining a two-item tuple for existing
consumers.
"""
[docs]
class Token(tuple[str, str]):
"""Metadata for one theme token.
``Token`` is a two-item ``(property, description)`` tuple. Existing unpacking, indexing,
equality checks, and tuple type checks therefore keep working; new consumers can use named
accessors and read :attr:`fallback` directly.
"""
fallback: str | None
def __new__(cls, property: str, description: str, *, fallback: str | None = None) -> "Token":
token = super().__new__(cls, (property, description))
object.__setattr__(token, "fallback", fallback)
return token
def __setattr__(self, name: str, value: object) -> None:
raise AttributeError(f"{type(self).__name__!s} is immutable")
def __getnewargs_ex__(self) -> tuple[tuple[str, str], dict[str, str | None]]:
return (self.property, self.description), {"fallback": self.fallback}
@property
def description(self) -> str:
return self[1]
@property
def property(self) -> str:
return self[0]
#: ``css()`` kwarg → (CSS custom property, what it controls). The shell reads these (see ``js shell.ts``).
SHELL_TOKENS = {
"spa_surface": Token("--spa-surface", "nav / footer / app surface color"),
"spa_surface_2": Token("--spa-surface-2", "gutter / toolbar surface color"),
"spa_border": Token("--spa-border", "shell border color"),
"spa_text": Token("--spa-text", "body text color"),
"spa_muted": Token("--spa-muted", "footer / muted text color"),
"spa_accent": Token("--spa-accent", "emphasis / hover / selection color"),
"spa_info": Token("--spa-info", "info tone (Toast, component packages)"),
"spa_success": Token("--spa-success", "success tone (Toast, component packages)"),
"spa_warning": Token("--spa-warning", "warning tone (component packages)"),
"spa_danger": Token("--spa-danger", "danger tone (Toast, component packages)"),
"spa_gap": Token("--spa-gap", "default gap between shell children"),
"spa_align": Token("--spa-align", "cross-axis alignment for Stack / Row / Toolbar"),
"spa_justify": Token("--spa-justify", "main-axis justification for Row"),
"spa_gutter_width": Token("--spa-gutter-width", "Gutter width"),
}
#: The prefix a component package's own tokens use: ``--spa-<package>-<thing>``.
PACKAGE_TOKEN_PREFIX = "--spa-"
def package_token(package: str, name: str) -> str:
"""The CSS custom property a component package exposes for one themeable thing.
``package_token("dagre", "node-fill")`` → ``"--spa-dagre-node-fill"``. Packages publish their
own ``TOKENS`` mapping; this is the naming rule those mappings follow.
"""
return f"{PACKAGE_TOKEN_PREFIX}{package}-{name}"