lightfall-utils 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ """Theme token definitions and the theme-provider interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+
10
+ @dataclass
11
+ class ThemeDefinition:
12
+ """Color and style definitions for a theme.
13
+
14
+ Attributes:
15
+ primary: Primary brand/accent color.
16
+ secondary: Secondary accent color.
17
+ success: Success/positive state color.
18
+ warning: Warning state color.
19
+ error: Error/danger state color.
20
+ info: Informational state color.
21
+ background: Main background color.
22
+ surface: Elevated surface color.
23
+ text: Primary text color.
24
+ text_secondary: Secondary/muted text color.
25
+ border: Border/divider color.
26
+ connected: Connected state color (defaults to success).
27
+ disconnected: Disconnected state color (defaults to error).
28
+ css_overrides: Optional CSS rules to append after base stylesheet.
29
+ """
30
+
31
+ primary: str = "#2563eb" # Blue
32
+ secondary: str = "#7c3aed" # Purple
33
+ success: str = "#16a34a" # Green
34
+ warning: str = "#d97706" # Amber
35
+ error: str = "#dc2626" # Red
36
+ info: str = "#0891b2" # Cyan
37
+
38
+ background: str = "#ffffff"
39
+ surface: str = "#f3f4f6"
40
+ text: str = "#1f2937"
41
+ text_secondary: str = "#6b7280"
42
+ border: str = "#e5e7eb"
43
+
44
+ connected: str = ""
45
+ disconnected: str = ""
46
+
47
+ # Islands layout: "sea" is the visible gap behind floating panels.
48
+ # When empty, falls back to background (non-Islands themes unchanged).
49
+ sea: str = ""
50
+
51
+ css_overrides: str = ""
52
+
53
+ def __post_init__(self) -> None:
54
+ """Set default state colors based on theme."""
55
+ if not self.connected:
56
+ self.connected = self.success
57
+ if not self.disconnected:
58
+ self.disconnected = self.error
59
+
60
+
61
+ class ThemeProvider(ABC):
62
+ """A named theme that supplies a ThemeDefinition.
63
+
64
+ Framework-free: host applications may mix this into their own plugin
65
+ base classes (Lightfall mixes it with its PluginType).
66
+ """
67
+
68
+ @property
69
+ @abstractmethod
70
+ def name(self) -> str:
71
+ """Unique identifier (lowercase, no spaces, e.g. "slate")."""
72
+ ...
73
+
74
+ @property
75
+ @abstractmethod
76
+ def display_name(self) -> str:
77
+ """Human-readable name shown in a theme selector."""
78
+ ...
79
+
80
+ @property
81
+ @abstractmethod
82
+ def is_dark(self) -> bool:
83
+ """Whether this is a dark theme (used for system-theme detection)."""
84
+ ...
85
+
86
+ @abstractmethod
87
+ def get_theme_definition(self) -> ThemeDefinition:
88
+ """Get the theme's color definitions."""
89
+ ...
90
+
91
+ def get_introspection_data(self) -> dict[str, Any]:
92
+ """Machine-readable summary of this theme."""
93
+ definition = self.get_theme_definition()
94
+ return {
95
+ "type": getattr(self, "type_name", "theme"),
96
+ "name": self.name,
97
+ "display_name": self.display_name,
98
+ "is_dark": self.is_dark,
99
+ "has_css_overrides": bool(definition.css_overrides),
100
+ "class": self.__class__.__name__,
101
+ "module": self.__class__.__module__,
102
+ }
@@ -0,0 +1,195 @@
1
+ """Theme registry for managing registered theme plugins.
2
+
3
+ ThemeRegistry is a singleton that manages all registered theme plugins.
4
+ It provides lookup by name and returns available themes for the UI.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import threading
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from lightfall_utils.logging import logger
13
+
14
+ if TYPE_CHECKING:
15
+ from lightfall_utils.theming.provider import ThemeDefinition, ThemeProvider
16
+
17
+
18
+ class ThemeRegistry:
19
+ """Singleton registry for theme plugins.
20
+
21
+ The registry manages registered theme plugins and provides:
22
+ - Registration of theme plugins
23
+ - Lookup by theme name
24
+ - List of available themes for UI
25
+ - Default theme selection based on system preference
26
+
27
+ Example::
28
+
29
+ registry = ThemeRegistry.get_instance()
30
+
31
+ # Register a theme
32
+ registry.register(my_theme_plugin)
33
+
34
+ # Get a theme by name
35
+ theme = registry.get("slate")
36
+
37
+ # Get all themes for UI dropdown
38
+ themes = registry.get_all()
39
+
40
+ # Get default theme based on system
41
+ theme = registry.get_theme_for_system(is_dark=True)
42
+ """
43
+
44
+ _instance: ThemeRegistry | None = None
45
+ _lock = threading.RLock()
46
+
47
+ def __init__(self) -> None:
48
+ """Initialize the theme registry."""
49
+ self._themes: dict[str, ThemeProvider] = {}
50
+
51
+ @classmethod
52
+ def get_instance(cls) -> ThemeRegistry:
53
+ """Get the singleton ThemeRegistry instance."""
54
+ if cls._instance is None:
55
+ with cls._lock:
56
+ if cls._instance is None:
57
+ cls._instance = cls()
58
+ return cls._instance
59
+
60
+ @classmethod
61
+ def reset(cls) -> None:
62
+ """Reset the singleton instance (for testing)."""
63
+ with cls._lock:
64
+ cls._instance = None
65
+
66
+ def register(self, plugin: ThemeProvider) -> bool:
67
+ """Register a theme plugin.
68
+
69
+ Args:
70
+ plugin: The theme plugin to register.
71
+
72
+ Returns:
73
+ True if registered successfully, False if name already exists.
74
+ """
75
+ name = plugin.name
76
+ if name in self._themes:
77
+ logger.warning("Theme '{}' already registered, skipping", name)
78
+ return False
79
+
80
+ self._themes[name] = plugin
81
+ logger.debug("Registered theme: {} ({})", name, plugin.display_name)
82
+ return True
83
+
84
+ def unregister(self, name: str) -> bool:
85
+ """Unregister a theme by name.
86
+
87
+ Args:
88
+ name: Theme name to unregister.
89
+
90
+ Returns:
91
+ True if unregistered, False if not found.
92
+ """
93
+ if name in self._themes:
94
+ del self._themes[name]
95
+ logger.debug("Unregistered theme: {}", name)
96
+ return True
97
+ return False
98
+
99
+ def get(self, name: str) -> ThemeProvider | None:
100
+ """Get a theme plugin by name.
101
+
102
+ Args:
103
+ name: The theme name (e.g., "light", "slate").
104
+
105
+ Returns:
106
+ The theme plugin or None if not found.
107
+ """
108
+ return self._themes.get(name)
109
+
110
+ def get_definition(self, name: str) -> ThemeDefinition | None:
111
+ """Get a theme definition by name.
112
+
113
+ Convenience method that returns the ThemeDefinition directly.
114
+
115
+ Args:
116
+ name: The theme name.
117
+
118
+ Returns:
119
+ The theme definition or None if not found.
120
+ """
121
+ plugin = self.get(name)
122
+ if plugin:
123
+ return plugin.get_theme_definition()
124
+ return None
125
+
126
+ def get_all(self) -> list[ThemeProvider]:
127
+ """Get all registered themes.
128
+
129
+ Returns:
130
+ List of theme plugins, sorted by display name.
131
+ """
132
+ return sorted(self._themes.values(), key=lambda t: t.display_name)
133
+
134
+ def get_names(self) -> list[str]:
135
+ """Get all registered theme names.
136
+
137
+ Returns:
138
+ List of theme names.
139
+ """
140
+ return list(self._themes.keys())
141
+
142
+ def get_theme_for_system(self, is_dark: bool) -> ThemeProvider | None:
143
+ """Get the default theme based on system preference.
144
+
145
+ Args:
146
+ is_dark: True if system is in dark mode.
147
+
148
+ Returns:
149
+ The first matching theme (dark or light), or None if no themes registered.
150
+ """
151
+ # Find themes matching the requested mode
152
+ matching = [t for t in self._themes.values() if t.is_dark == is_dark]
153
+
154
+ if matching:
155
+ # Prefer "light" or "slate" as defaults, otherwise first match
156
+ for preferred in ("light", "slate"):
157
+ for theme in matching:
158
+ if theme.name == preferred:
159
+ return theme
160
+ return matching[0]
161
+
162
+ # Fall back to any theme
163
+ if self._themes:
164
+ return next(iter(self._themes.values()))
165
+
166
+ return None
167
+
168
+ def has_theme(self, name: str) -> bool:
169
+ """Check if a theme is registered.
170
+
171
+ Args:
172
+ name: Theme name to check.
173
+
174
+ Returns:
175
+ True if the theme exists.
176
+ """
177
+ return name in self._themes
178
+
179
+ def get_introspection_data(self) -> dict[str, Any]:
180
+ """Get introspection data for MCP tools.
181
+
182
+ Returns:
183
+ Dictionary with registry information.
184
+ """
185
+ return {
186
+ "registered_themes": [
187
+ {
188
+ "name": t.name,
189
+ "display_name": t.display_name,
190
+ "is_dark": t.is_dark,
191
+ }
192
+ for t in self.get_all()
193
+ ],
194
+ "count": len(self._themes),
195
+ }