gradio-themer 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,3 @@
1
+ from .gradio_themer import GradioThemer
2
+
3
+ __all__ = ["GradioThemer"]
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate theme template for Gradio Themer component
4
+ Usage: python generate_theme_template.py --name "My Theme" --background "#ffffff"
5
+ """
6
+
7
+ import json
8
+ import argparse
9
+ from pathlib import Path
10
+ from typing import Dict, Any
11
+
12
+
13
+ def generate_theme_template(
14
+ theme_name: str, background_color: str = "#ffffff"
15
+ ) -> Dict[str, Any]:
16
+ """
17
+ Generate a theme template with sensible defaults
18
+
19
+ Args:
20
+ theme_name: Display name for the theme
21
+ background_color: Main background color for the theme
22
+
23
+ Returns:
24
+ Dictionary containing the complete theme configuration
25
+ """
26
+ theme_key = theme_name.lower().replace(" ", "_").replace("-", "_")
27
+
28
+ template = {
29
+ "themes": {
30
+ theme_key: {
31
+ "name": theme_name,
32
+ "colors": {
33
+ # Base colors - main backgrounds and text
34
+ "base-100": "#ffffff", # Main background
35
+ "base-200": "#f8fafc", # Secondary background
36
+ "base-300": "#e2e8f0", # Border color
37
+ "base-content": "#1e293b", # Main text color
38
+ # Primary colors - main action buttons
39
+ "primary": "#3b82f6", # Primary button color
40
+ "primary-content": "#ffffff", # Primary button text
41
+ # Secondary colors - secondary actions
42
+ "secondary": "#64748b", # Secondary button color
43
+ "secondary-content": "#ffffff", # Secondary button text
44
+ # Accent colors - highlights and special elements
45
+ "accent": "#f59e0b", # Accent color
46
+ "accent-content": "#000000", # Accent text color
47
+ # Neutral colors - neutral elements
48
+ "neutral": "#374151", # Neutral color
49
+ "neutral-content": "#ffffff", # Neutral text color
50
+ # Status colors
51
+ "error": "#ef4444", # Error color
52
+ "error-content": "#ffffff", # Error text color
53
+ },
54
+ "background": background_color, # Overall theme background
55
+ }
56
+ },
57
+ "default_theme": theme_key,
58
+ "default_font": "Inter",
59
+ }
60
+
61
+ return template
62
+
63
+
64
+ def add_theme_to_existing_config(
65
+ config_path: str, theme_name: str, background_color: str = "#ffffff"
66
+ ) -> bool:
67
+ """
68
+ Add a new theme to an existing configuration file
69
+
70
+ Args:
71
+ config_path: Path to existing configuration file
72
+ theme_name: Name of the new theme
73
+ background_color: Background color for the theme
74
+
75
+ Returns:
76
+ True if successful, False otherwise
77
+ """
78
+ try:
79
+ # Load existing config
80
+ with open(config_path, "r") as f:
81
+ config = json.load(f)
82
+
83
+ # Generate new theme
84
+ new_template = generate_theme_template(theme_name, background_color)
85
+ theme_key = list(new_template["themes"].keys())[0]
86
+
87
+ # Add to existing themes
88
+ if "themes" not in config:
89
+ config["themes"] = {}
90
+
91
+ config["themes"][theme_key] = new_template["themes"][theme_key]
92
+
93
+ # Write back to file
94
+ with open(config_path, "w") as f:
95
+ json.dump(config, f, indent=2)
96
+
97
+ return True
98
+
99
+ except Exception as e:
100
+ print(f"❌ Error adding theme to existing config: {e}")
101
+ return False
102
+
103
+
104
+ def main():
105
+ parser = argparse.ArgumentParser(
106
+ description="Generate Gradio theme template",
107
+ formatter_class=argparse.RawDescriptionHelpFormatter,
108
+ epilog="""
109
+ Examples:
110
+ # Generate new theme config file
111
+ python generate_theme_template.py --name "My Theme"
112
+
113
+ # Generate with custom background
114
+ python generate_theme_template.py --name "Dark Theme" --background "#1f2937"
115
+
116
+ # Add to existing config file
117
+ python generate_theme_template.py --name "New Theme" --add-to existing_themes.json
118
+
119
+ # Generate with custom output path
120
+ python generate_theme_template.py --name "Corporate" --output corporate_themes.json
121
+ """,
122
+ )
123
+
124
+ parser.add_argument("--name", required=True, help="Theme display name")
125
+ parser.add_argument(
126
+ "--background", default="#ffffff", help="Background color (default: #ffffff)"
127
+ )
128
+ parser.add_argument(
129
+ "--output",
130
+ default="user_themes.json",
131
+ help="Output file path (default: user_themes.json)",
132
+ )
133
+ parser.add_argument(
134
+ "--add-to",
135
+ dest="add_to",
136
+ help="Add theme to existing config file instead of creating new one",
137
+ )
138
+
139
+ args = parser.parse_args()
140
+
141
+ if args.add_to:
142
+ # Add to existing configuration
143
+ if not Path(args.add_to).exists():
144
+ print(f"❌ Configuration file {args.add_to} does not exist")
145
+ return 1
146
+
147
+ if add_theme_to_existing_config(args.add_to, args.name, args.background):
148
+ print(f"✅ Added theme '{args.name}' to {args.add_to}")
149
+ print(f"📝 Edit the file to customize colors further")
150
+ else:
151
+ print(f"❌ Failed to add theme to {args.add_to}")
152
+ return 1
153
+ else:
154
+ # Generate new configuration file
155
+ template = generate_theme_template(args.name, args.background)
156
+
157
+ # Write to file
158
+ with open(args.output, "w") as f:
159
+ json.dump(template, f, indent=2)
160
+
161
+ print(f"✅ Generated theme template: {args.output}")
162
+ print(f"📝 Edit the file to customize colors and add more themes")
163
+ print(f"🎨 Theme key: {list(template['themes'].keys())[0]}")
164
+
165
+ return 0
166
+
167
+
168
+ if __name__ == "__main__":
169
+ exit(main())
@@ -0,0 +1,336 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Callable, Sequence, Optional
7
+ from gradio.components.base import FormComponent
8
+ from gradio.events import Events
9
+
10
+
11
+ class GradioThemer(FormComponent):
12
+ """
13
+ A custom Gradio component for applying user-configurable themes to Gradio applications.
14
+
15
+ This component allows users to:
16
+ - Configure unlimited custom themes via JSON configuration files
17
+ - Preview themes with live Gradio components
18
+ - Switch between themes dynamically
19
+ - Export CSS for use in other projects
20
+ """
21
+
22
+ EVENTS = [
23
+ Events.change,
24
+ Events.input,
25
+ Events.submit,
26
+ ]
27
+
28
+ def __init__(
29
+ self,
30
+ value: Dict[str, Any] | Callable | None = None,
31
+ theme_config_path: Optional[str] = None,
32
+ *,
33
+ label: str | None = None,
34
+ every: float | None = None,
35
+ inputs: (
36
+ FormComponent | Sequence[FormComponent] | set[FormComponent] | None
37
+ ) = None,
38
+ show_label: bool | None = None,
39
+ scale: int | None = None,
40
+ min_width: int = 160,
41
+ interactive: bool | None = None,
42
+ visible: bool = True,
43
+ elem_id: str | None = None,
44
+ elem_classes: list[str] | str | None = None,
45
+ render: bool = True,
46
+ key: int | str | None = None,
47
+ ):
48
+ """
49
+ Parameters:
50
+ value: Default theme configuration. Should be a dict with 'themeInput', 'themeConfig', and 'generatedCSS' keys.
51
+ theme_config_path: Path to user themes configuration file (JSON). If None, searches for common filenames.
52
+ label: The label for this component, displayed above the component if `show_label` is `True`.
53
+ every: Continously calls `value` to recalculate it if `value` is a function.
54
+ inputs: Components that are used as inputs to calculate `value` if `value` is a function.
55
+ show_label: If True, will display label.
56
+ scale: Relative size compared to adjacent Components.
57
+ min_width: Minimum pixel width.
58
+ interactive: If True, will be rendered as an editable component.
59
+ visible: If False, component will be hidden.
60
+ elem_id: An optional string that is assigned as the id of this component in the HTML DOM.
61
+ elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM.
62
+ render: If False, component will not render be rendered in the Blocks context.
63
+ key: A unique key for this component.
64
+ """
65
+ # Load user themes from configuration before calling super().__init__
66
+ self.user_themes = self._load_user_themes(theme_config_path)
67
+
68
+ super().__init__(
69
+ label=label,
70
+ every=every,
71
+ inputs=inputs,
72
+ show_label=show_label,
73
+ scale=scale,
74
+ min_width=min_width,
75
+ interactive=interactive,
76
+ visible=visible,
77
+ elem_id=elem_id,
78
+ elem_classes=elem_classes,
79
+ value=value,
80
+ render=render,
81
+ key=key,
82
+ )
83
+
84
+ def _load_user_themes(self, config_path: Optional[str] = None) -> Dict[str, Any]:
85
+ """
86
+ Load themes from user configuration file
87
+
88
+ Args:
89
+ config_path: Optional path to theme configuration file
90
+
91
+ Returns:
92
+ Dictionary containing user themes, or built-in themes as fallback
93
+ """
94
+ # Default paths to search for theme config
95
+ search_paths = [
96
+ config_path,
97
+ "user_themes.json",
98
+ "themes.json",
99
+ "gradio_themes.json",
100
+ os.path.expanduser("~/.gradio/gradio_themes.json"),
101
+ # Also check in the component's directory for the example file
102
+ os.path.join(
103
+ os.path.dirname(__file__), "..", "..", "user_themes_example.json"
104
+ ),
105
+ ]
106
+
107
+ for path in search_paths:
108
+ if path and Path(path).exists():
109
+ try:
110
+ with open(path, "r") as f:
111
+ config = json.load(f)
112
+ themes = config.get("themes", {})
113
+ if themes:
114
+ print(f"✅ Loaded {len(themes)} user themes from {path}")
115
+ return themes
116
+ except Exception as e:
117
+ print(f"⚠️ Error loading theme config from {path}: {e}")
118
+ continue
119
+
120
+ # Return built-in fallback themes if no config found
121
+ print("📝 No user theme config found, using built-in themes")
122
+ return self._get_builtin_themes()
123
+
124
+ def _get_builtin_themes(self) -> Dict[str, Any]:
125
+ """Get the built-in fallback themes"""
126
+ return {
127
+ "corporate": {
128
+ "name": "Corporate",
129
+ "colors": {
130
+ "base-100": "oklch(100% 0 0)",
131
+ "base-200": "oklch(96% 0.02 276.935)",
132
+ "base-300": "oklch(90% 0.05 293.541)",
133
+ "base-content": "oklch(22.389% 0.031 278.072)",
134
+ "primary": "oklch(58% 0.158 241.966)",
135
+ "primary-content": "oklch(100% 0 0)",
136
+ "secondary": "oklch(55% 0.046 257.417)",
137
+ "secondary-content": "oklch(100% 0 0)",
138
+ "accent": "oklch(60% 0.118 184.704)",
139
+ "accent-content": "oklch(100% 0 0)",
140
+ "neutral": "oklch(0% 0 0)",
141
+ "neutral-content": "oklch(100% 0 0)",
142
+ "error": "oklch(70% 0.191 22.216)",
143
+ "error-content": "oklch(0% 0 0)",
144
+ },
145
+ "background": "#06b6d4",
146
+ },
147
+ "cupcake": {
148
+ "name": "Cupcake",
149
+ "colors": {
150
+ "base-100": "oklch(100% 0 0)",
151
+ "base-200": "oklch(96% 0.014 340.77)",
152
+ "base-300": "oklch(92% 0.021 340.77)",
153
+ "base-content": "oklch(22.389% 0.031 278.072)",
154
+ "primary": "oklch(65.69% 0.196 342.55)",
155
+ "primary-content": "oklch(100% 0 0)",
156
+ "secondary": "oklch(74.51% 0.167 183.61)",
157
+ "secondary-content": "oklch(100% 0 0)",
158
+ "accent": "oklch(74.51% 0.167 183.61)",
159
+ "accent-content": "oklch(100% 0 0)",
160
+ "neutral": "oklch(22.389% 0.031 278.072)",
161
+ "neutral-content": "oklch(100% 0 0)",
162
+ "error": "oklch(70% 0.191 22.216)",
163
+ "error-content": "oklch(0% 0 0)",
164
+ },
165
+ "background": "#faf0e6",
166
+ },
167
+ "dark": {
168
+ "name": "Dark",
169
+ "colors": {
170
+ "base-100": "oklch(25.3% 0.015 252.417)",
171
+ "base-200": "oklch(22.2% 0.013 252.417)",
172
+ "base-300": "oklch(19.1% 0.011 252.417)",
173
+ "base-content": "oklch(74.6% 0.019 83.916)",
174
+ "primary": "oklch(65.69% 0.196 275.75)",
175
+ "primary-content": "oklch(100% 0 0)",
176
+ "secondary": "oklch(74.51% 0.167 183.61)",
177
+ "secondary-content": "oklch(100% 0 0)",
178
+ "accent": "oklch(74.51% 0.167 183.61)",
179
+ "accent-content": "oklch(100% 0 0)",
180
+ "neutral": "oklch(25.3% 0.015 252.417)",
181
+ "neutral-content": "oklch(74.6% 0.019 83.916)",
182
+ "error": "oklch(70% 0.191 22.216)",
183
+ "error-content": "oklch(0% 0 0)",
184
+ },
185
+ "background": "#1f2937",
186
+ },
187
+ "emerald": {
188
+ "name": "Emerald",
189
+ "colors": {
190
+ "base-100": "oklch(100% 0 0)",
191
+ "base-200": "oklch(96% 0.014 154.77)",
192
+ "base-300": "oklch(92% 0.021 154.77)",
193
+ "base-content": "oklch(22.389% 0.031 278.072)",
194
+ "primary": "oklch(65.69% 0.196 162.55)",
195
+ "primary-content": "oklch(100% 0 0)",
196
+ "secondary": "oklch(74.51% 0.167 183.61)",
197
+ "secondary-content": "oklch(100% 0 0)",
198
+ "accent": "oklch(74.51% 0.167 183.61)",
199
+ "accent-content": "oklch(100% 0 0)",
200
+ "neutral": "oklch(22.389% 0.031 278.072)",
201
+ "neutral-content": "oklch(100% 0 0)",
202
+ "error": "oklch(70% 0.191 22.216)",
203
+ "error-content": "oklch(0% 0 0)",
204
+ },
205
+ "background": "#ecfdf5",
206
+ },
207
+ }
208
+
209
+ def preprocess(self, payload: Dict[str, Any] | None) -> Dict[str, Any] | None:
210
+ """
211
+ Parameters:
212
+ payload: The theme configuration data from the frontend.
213
+ Returns:
214
+ Passes the theme configuration as a dict into the function.
215
+ """
216
+ if payload is None:
217
+ return None
218
+
219
+ # Ensure we have the expected structure
220
+ if isinstance(payload, dict):
221
+ # Handle different input formats
222
+ result = {
223
+ "themeInput": payload.get("themeInput", ""),
224
+ "themeConfig": payload.get("themeConfig"),
225
+ "generatedCSS": payload.get("generatedCSS", ""),
226
+ }
227
+
228
+ # Include additional fields if present
229
+ if "currentTheme" in payload:
230
+ result["currentTheme"] = payload["currentTheme"]
231
+ if "type" in payload:
232
+ result["type"] = payload["type"]
233
+
234
+ return result
235
+
236
+ return None
237
+
238
+ def postprocess(self, value: Dict[str, Any] | None) -> Dict[str, Any] | None:
239
+ """
240
+ Parameters:
241
+ value: Expects a dict with theme configuration data.
242
+ Returns:
243
+ The value to display in the component, including user themes.
244
+ """
245
+ if value is None:
246
+ result = self._get_default_value()
247
+ elif isinstance(value, dict):
248
+ # Handle different input formats
249
+ if "currentTheme" in value:
250
+ # Handle theme selection format
251
+ theme_name = value.get("currentTheme", "light")
252
+ result = {
253
+ "currentTheme": theme_name,
254
+ "themeInput": value.get("themeInput", ""),
255
+ "themeConfig": value.get("themeConfig"),
256
+ "generatedCSS": value.get("generatedCSS", ""),
257
+ "type": value.get("type", "builtin"),
258
+ "font": value.get(
259
+ "font",
260
+ {"family": "Inter", "weights": ["400", "500", "600", "700"]},
261
+ ),
262
+ "removeBorders": value.get("removeBorders", True),
263
+ }
264
+ else:
265
+ # Handle raw theme configuration format
266
+ result = {
267
+ "themeInput": value.get("themeInput", ""),
268
+ "themeConfig": value.get("themeConfig"),
269
+ "generatedCSS": value.get("generatedCSS", ""),
270
+ "font": value.get(
271
+ "font",
272
+ {"family": "Inter", "weights": ["400", "500", "600", "700"]},
273
+ ),
274
+ "removeBorders": value.get("removeBorders", True),
275
+ }
276
+ else:
277
+ result = self._get_default_value()
278
+
279
+ # Inject user themes into the result for frontend consumption
280
+ result["available_themes"] = self.user_themes
281
+
282
+ return result
283
+
284
+ def _get_default_value(self) -> Dict[str, Any]:
285
+ """Get the default theme configuration"""
286
+ emerald_theme = """@theme "emerald" {
287
+ name: "emerald";
288
+ default: true;
289
+ prefersdark: false;
290
+ color-scheme: "light";
291
+ --color-base-100: oklch(100% 0 0);
292
+ --color-base-200: oklch(93% 0 0);
293
+ --color-base-300: oklch(86% 0 0);
294
+ --color-base-content: oklch(35.519% 0.032 262.988);
295
+ --color-primary: oklch(76.662% 0.135 153.45);
296
+ --color-primary-content: oklch(33.387% 0.04 162.24);
297
+ --color-secondary: oklch(61.302% 0.202 261.294);
298
+ --color-secondary-content: oklch(100% 0 0);
299
+ --color-accent: oklch(72.772% 0.149 33.2);
300
+ --color-accent-content: oklch(0% 0 0);
301
+ --color-neutral: oklch(35.519% 0.032 262.988);
302
+ --color-neutral-content: oklch(98.462% 0.001 247.838);
303
+ --color-info: oklch(72.06% 0.191 231.6);
304
+ --color-info-content: oklch(0% 0 0);
305
+ --color-success: oklch(64.8% 0.15 160);
306
+ --color-success-content: oklch(0% 0 0);
307
+ --color-warning: oklch(84.71% 0.199 83.87);
308
+ --color-warning-content: oklch(0% 0 0);
309
+ --color-error: oklch(71.76% 0.221 22.18);
310
+ --color-error-content: oklch(0% 0 0);
311
+ --radius-selector: 1rem;
312
+ --radius-field: 0.5rem;
313
+ --radius-box: 1rem;
314
+ --size-selector: 0.25rem;
315
+ --size-field: 0.25rem;
316
+ --border: 1px;
317
+ --depth: 1;
318
+ --noise: 1;
319
+ }"""
320
+
321
+ return {"themeInput": emerald_theme, "themeConfig": None, "generatedCSS": ""}
322
+
323
+ def example_payload(self) -> Any:
324
+ return {
325
+ "themeInput": "sample theme",
326
+ "generatedCSS": ":root { --color-primary: blue; }",
327
+ }
328
+
329
+ def example_value(self) -> Any:
330
+ return {
331
+ "themeInput": "sample theme",
332
+ "generatedCSS": ":root { --color-primary: blue; }",
333
+ }
334
+
335
+ def api_info(self) -> dict[str, Any]:
336
+ return {"type": {}, "description": "Gradio theme configuration object"}