[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/pythonnative/pythonnative/main/src/pythonnative/theme.py [Back]  [Original]

"""Design tokens shared by the application and the navigators.

A [`Theme`][pythonnative.Theme] is a frozen, keyword-only dataclass:
whether it's dark, a [`Colors`][pythonnative.Colors] palette,
[`Typography`][pythonnative.Typography] text styles, a
[`Spacing`][pythonnative.Spacing] scale, and [`Radii`][pythonnative.Radii].
Components read it with [`use_theme`][pythonnative.use_theme]; the
built-in navigators draw their headers, tab bars, and drawers from the
same tokens, so one theme styles the whole app.

Without a provider, the theme follows the system appearance:
[`LIGHT_THEME`][pythonnative.LIGHT_THEME] or
[`DARK_THEME`][pythonnative.DARK_THEME]. Provide your own pair with
[`ThemeProvider`][pythonnative.ThemeProvider], and add tokens by
subclassing:

```python
from dataclasses import dataclass, replace

import pythonnative as pn


@dataclass(frozen=True, kw_only=True)
class BrandTheme(pn.Theme):
    accent: pn.Color = "#FF2D55"


LIGHT = BrandTheme(colors=replace(pn.LIGHT_THEME.colors, primary="#5B21B6"))
DARK = BrandTheme(dark=True, colors=replace(pn.DARK_THEME.colors, primary="#A78BFA"), accent="#FF6482")


@pn.component
def App() -> pn.Node:
    return pn.ThemeProvider(Home(), light=LIGHT, dark=DARK)


@pn.component
def Home() -> pn.Node:
    theme = pn.use_theme(BrandTheme)
    return pn.Text("Hello", style={"color": theme.accent})
```

Styles that depend on the theme are derived once per theme with
[`use_styles`][pythonnative.use_styles].
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Callable, Optional, TypeVar, overload

from .component import component
from .element import Element, Node
from .hooks import Context, create_context, use_color_scheme, use_context, use_memo
from .style import Color, Style

__all__ = [
    "DARK_THEME",
    "LIGHT_THEME",
    "Colors",
    "Radii",
    "Spacing",
    "Theme",
    "ThemeProvider",
    "Typography",
    "use_styles",
    "use_theme",
]

ThemeT = TypeVar("ThemeT", bound="Theme")
S = TypeVar("S")


@dataclass(frozen=True, kw_only=True)
class Colors:
    """A theme's color palette.

    Attributes:
        primary: Accent for interactive elements. Navigators use it for
            the back button, header buttons, the selected tab, and the
            active drawer row.
        background: Screen background.
        surface: Raised surfaces such as cards, sheets, the navigation
            header, the tab bar, and the drawer panel.
        text: Primary text, including header titles.
        text_secondary: De-emphasized text.
        border: Hairlines and dividers, including the ones between the
            header or tab bar and the content.
        error: Destructive and error states, and tab badges.
        success: Success states.
        warning: Warning states.
    """

    primary: Color
    background: Color
    surface: Color
    text: Color
    text_secondary: Color
    border: Color
    error: Color
    success: Color
    warning: Color


def _text(**style: Any) -> Any:
    return field(default_factory=lambda: dict(style))


@dataclass(frozen=True, kw_only=True)
class Typography:
    """Named text styles; each is a [`Style`][pythonnative.Style] to pass to ``pn.Text``.

    Attributes:
        title: Screen titles.
        heading: Section headings.
        body: Body text.
        label: Labels on controls and list rows.
        caption: Captions and footnotes.
    """

    title: Style = _text(font_size=28, font_weight="700")
    heading: Style = _text(font_size=20, font_weight="600")
    body: Style = _text(font_size=16)
    label: Style = _text(font_size=15, font_weight="500")
    caption: Style = _text(font_size=13)


@dataclass(frozen=True, kw_only=True)
class Spacing:
    """A spacing scale in points, for padding, margins, and ``gap``."""

    xs: float = 4
    sm: float = 8
    md: float = 16
    lg: float = 24
    xl: float = 32


@dataclass(frozen=True, kw_only=True)
class Radii:
    """Corner radii in points; ``full`` makes a pill or circle."""

    sm: float = 4
    md: float = 8
    lg: float = 16
    full: float = 9999


@dataclass(frozen=True, kw_only=True)
class Theme:
    """Design tokens read through [`use_theme`][pythonnative.use_theme].

    Subclass it (keeping ``frozen=True, kw_only=True``) to add
    application tokens, and derive variations with
    [`dataclasses.replace`][dataclasses.replace].

    Attributes:
        dark: Whether the theme is meant for a dark appearance.
        colors: The [`Colors`][pythonnative.Colors] palette.
        typography: Named [`Typography`][pythonnative.Typography] text styles.
        spacing: The [`Spacing`][pythonnative.Spacing] scale.
        radii: Corner [`Radii`][pythonnative.Radii].
    """

    dark: bool = False
    colors: Colors
    typography: Typography = field(default_factory=Typography)
    spacing: Spacing = field(default_factory=Spacing)
    radii: Radii = field(default_factory=Radii)


LIGHT_THEME = Theme(
    colors=Colors(
        primary="#007AFF",
        background="#F2F2F7",
        surface="#FFFFFF",
        text="#000000",
        text_secondary="#6C6C70",
        border="#D1D1D6",
        error="#FF3B30",
        success="#34C759",
        warning="#FF9500",
    ),
)
"""The built-in light theme, used when the system appearance is light."""

DARK_THEME = Theme(
    dark=True,
    colors=Colors(
        primary="#0A84FF",
        background="#000000",
        surface="#1C1C1E",
        text="#FFFFFF",
        text_secondary="#98989F",
        border="#38383A",
        error="#FF453A",
        success="#30D158",
        warning="#FF9F0A",
    ),
)
"""The built-in dark theme, used when the system appearance is dark."""

_ThemeContext: Context[Optional[Theme]] = create_context(None, name="Theme")


@component
def ThemeProvider(*children: Node, light: Theme = LIGHT_THEME, dark: Theme = DARK_THEME) -> Element:
    """Provide ``light`` or ``dark`` to the subtree, following the effective color scheme.

    The effective scheme is the system appearance unless the app
    overrides it with
    [`appearance.set_color_scheme`][pythonnative.appearance.set_color_scheme].
    Pass the same theme for both to pin one regardless of appearance.

    Args:
        *children: The subtree that reads the theme.
        light: Theme used when the scheme is light.
        dark: Theme used when the scheme is dark.
    """
    scheme = use_color_scheme()
    return _ThemeContext.Provider(*children, value=dark if scheme == "dark" else light)


@overload
def use_theme() -> Theme: ...


@overload
def use_theme(kind: type[ThemeT], /) -> ThemeT: ...


def use_theme(kind: Optional[type[Theme]] = None, /) -> Theme:
    """Return the active [`Theme`][pythonnative.Theme] and re-render when it changes.

    The nearest [`ThemeProvider`][pythonnative.ThemeProvider] wins;
    without one, the built-in light or dark theme follows the effective
    color scheme.

    Args:
        kind: Optional ``Theme`` subclass. The return value is typed as
            that subclass, and a provided theme of another type raises.

    Raises:
        TypeError: If ``kind`` is given and the active theme isn't an
            instance of it.
        RuntimeError: If called outside a ``@component`` function.

    Example:
        ```python
        @pn.component
        def Card(*children: pn.Node) -> pn.Node:
            theme = pn.use_theme()
            return pn.View(
                *children,
                style={"background_color": theme.colors.surface, "padding": theme.spacing.md},
            )
        ```
    """
    scheme = use_color_scheme()
    provided = use_context(_ThemeContext)
    theme = provided if provided is not None else (DARK_THEME if scheme == "dark" else LIGHT_THEME)
    if kind is not None and not isinstance(theme, kind):
        raise TypeError(
            f"use_theme({kind.__name__}) found a {type(theme).__name__}; wrap the app in "
            f"pn.ThemeProvider(..., light=..., dark=...) with {kind.__name__} instances."
        )
    return theme


def use_styles(factory: Callable[[ThemeT], S]) -> S:
    """Return ``factory(theme)``, computed once per theme for this component.

    ``factory`` is usually a class whose ``__init__`` takes the theme
    and assigns styles as attributes, so they're typed and misspelled
    names are static errors:

    ```python
    class CardStyles:
        def __init__(self, theme: pn.Theme) -> None:
            self.card = pn.style(background_color=theme.colors.surface, border_radius=theme.radii.md)
            self.title = theme.typography.heading


    @pn.component
    def Card(title: str) -> pn.Node:
        styles = pn.use_styles(CardStyles)
        return pn.View(pn.Text(title, style=styles.title), style=styles.card)
    ```

    Args:
        factory: A callable taking the active theme.

    Returns:
        Whatever ``factory`` returns, with its type.
    """
    theme: Any = use_theme()
    return use_memo(lambda: factory(theme), [factory, theme])

Web Proxy Viewer  |  New URL  |  Original Page