nextpy-framework 1.0.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.
- nextpy/__init__.py +50 -0
- nextpy/auth.py +94 -0
- nextpy/builder.py +123 -0
- nextpy/cli.py +490 -0
- nextpy/components/__init__.py +45 -0
- nextpy/components/feedback.py +210 -0
- nextpy/components/form.py +346 -0
- nextpy/components/head.py +167 -0
- nextpy/components/hooks_provider.py +64 -0
- nextpy/components/image.py +180 -0
- nextpy/components/layout.py +206 -0
- nextpy/components/link.py +132 -0
- nextpy/components/loader.py +65 -0
- nextpy/components/toast.py +101 -0
- nextpy/components/visual.py +185 -0
- nextpy/config.py +75 -0
- nextpy/core/__init__.py +21 -0
- nextpy/core/builder.py +237 -0
- nextpy/core/data_fetching.py +221 -0
- nextpy/core/renderer.py +252 -0
- nextpy/core/router.py +233 -0
- nextpy/core/sync.py +34 -0
- nextpy/db.py +121 -0
- nextpy/dev_server.py +69 -0
- nextpy/dev_tools.py +157 -0
- nextpy/errors.py +70 -0
- nextpy/hooks.py +348 -0
- nextpy/performance.py +78 -0
- nextpy/plugins.py +61 -0
- nextpy/py.typed +0 -0
- nextpy/server/__init__.py +6 -0
- nextpy/server/app.py +325 -0
- nextpy/server/debug.py +93 -0
- nextpy/server/middleware.py +88 -0
- nextpy/utils/__init__.py +0 -0
- nextpy/utils/cache.py +89 -0
- nextpy/utils/email.py +59 -0
- nextpy/utils/file_upload.py +65 -0
- nextpy/utils/logging.py +52 -0
- nextpy/utils/search.py +59 -0
- nextpy/utils/seo.py +85 -0
- nextpy/utils/validators.py +58 -0
- nextpy/websocket.py +76 -0
- nextpy_framework-1.0.0.dist-info/METADATA +343 -0
- nextpy_framework-1.0.0.dist-info/RECORD +49 -0
- nextpy_framework-1.0.0.dist-info/WHEEL +5 -0
- nextpy_framework-1.0.0.dist-info/entry_points.txt +2 -0
- nextpy_framework-1.0.0.dist-info/licenses/LICENSE +21 -0
- nextpy_framework-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy Head Component - SEO and meta tag management
|
|
3
|
+
Similar to Next.js's next/head
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from markupsafe import Markup
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Head:
|
|
11
|
+
"""
|
|
12
|
+
Head component for managing document head elements
|
|
13
|
+
|
|
14
|
+
Usage in templates:
|
|
15
|
+
{{ Head(title="My Page", description="Page description") }}
|
|
16
|
+
|
|
17
|
+
Or in Python:
|
|
18
|
+
head = Head(title="My Page")
|
|
19
|
+
head.add_meta(name="author", content="John Doe")
|
|
20
|
+
print(head.render())
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
title: Optional[str] = None,
|
|
26
|
+
description: Optional[str] = None,
|
|
27
|
+
keywords: Optional[List[str]] = None,
|
|
28
|
+
canonical: Optional[str] = None,
|
|
29
|
+
og_title: Optional[str] = None,
|
|
30
|
+
og_description: Optional[str] = None,
|
|
31
|
+
og_image: Optional[str] = None,
|
|
32
|
+
og_type: str = "website",
|
|
33
|
+
twitter_card: str = "summary_large_image",
|
|
34
|
+
favicon: Optional[str] = None,
|
|
35
|
+
**kwargs: Any,
|
|
36
|
+
):
|
|
37
|
+
self.title = title
|
|
38
|
+
self.description = description
|
|
39
|
+
self.keywords = keywords or []
|
|
40
|
+
self.canonical = canonical
|
|
41
|
+
self.og_title = og_title or title
|
|
42
|
+
self.og_description = og_description or description
|
|
43
|
+
self.og_image = og_image
|
|
44
|
+
self.og_type = og_type
|
|
45
|
+
self.twitter_card = twitter_card
|
|
46
|
+
self.favicon = favicon
|
|
47
|
+
self.extra_meta: List[Dict[str, str]] = []
|
|
48
|
+
self.extra_links: List[Dict[str, str]] = []
|
|
49
|
+
self.extra_scripts: List[Dict[str, str]] = []
|
|
50
|
+
self.extra_kwargs = kwargs
|
|
51
|
+
|
|
52
|
+
def add_meta(self, **attrs: str) -> "Head":
|
|
53
|
+
"""Add a custom meta tag"""
|
|
54
|
+
self.extra_meta.append(attrs)
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def add_link(self, **attrs: str) -> "Head":
|
|
58
|
+
"""Add a custom link tag"""
|
|
59
|
+
self.extra_links.append(attrs)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def add_script(self, src: Optional[str] = None, **attrs: str) -> "Head":
|
|
63
|
+
"""Add a script tag"""
|
|
64
|
+
if src:
|
|
65
|
+
attrs["src"] = src
|
|
66
|
+
self.extra_scripts.append(attrs)
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def render(self) -> str:
|
|
70
|
+
"""Render the head elements as HTML"""
|
|
71
|
+
elements = []
|
|
72
|
+
|
|
73
|
+
if self.title:
|
|
74
|
+
elements.append(f"<title>{self._escape(self.title)}</title>")
|
|
75
|
+
|
|
76
|
+
elements.append('<meta charset="UTF-8">')
|
|
77
|
+
elements.append('<meta name="viewport" content="width=device-width, initial-scale=1.0">')
|
|
78
|
+
|
|
79
|
+
if self.description:
|
|
80
|
+
elements.append(
|
|
81
|
+
f'<meta name="description" content="{self._escape(self.description)}">'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if self.keywords:
|
|
85
|
+
keywords_str = ", ".join(self.keywords)
|
|
86
|
+
elements.append(f'<meta name="keywords" content="{self._escape(keywords_str)}">')
|
|
87
|
+
|
|
88
|
+
if self.canonical:
|
|
89
|
+
elements.append(f'<link rel="canonical" href="{self._escape(self.canonical)}">')
|
|
90
|
+
|
|
91
|
+
if self.og_title:
|
|
92
|
+
elements.append(f'<meta property="og:title" content="{self._escape(self.og_title)}">')
|
|
93
|
+
if self.og_description:
|
|
94
|
+
elements.append(
|
|
95
|
+
f'<meta property="og:description" content="{self._escape(self.og_description)}">'
|
|
96
|
+
)
|
|
97
|
+
if self.og_image:
|
|
98
|
+
elements.append(f'<meta property="og:image" content="{self._escape(self.og_image)}">')
|
|
99
|
+
if self.og_type:
|
|
100
|
+
elements.append(f'<meta property="og:type" content="{self._escape(self.og_type)}">')
|
|
101
|
+
|
|
102
|
+
if self.twitter_card:
|
|
103
|
+
elements.append(f'<meta name="twitter:card" content="{self._escape(self.twitter_card)}">')
|
|
104
|
+
if self.og_title:
|
|
105
|
+
elements.append(
|
|
106
|
+
f'<meta name="twitter:title" content="{self._escape(self.og_title)}">'
|
|
107
|
+
)
|
|
108
|
+
if self.og_description:
|
|
109
|
+
elements.append(
|
|
110
|
+
f'<meta name="twitter:description" content="{self._escape(self.og_description)}">'
|
|
111
|
+
)
|
|
112
|
+
if self.og_image:
|
|
113
|
+
elements.append(
|
|
114
|
+
f'<meta name="twitter:image" content="{self._escape(self.og_image)}">'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if self.favicon:
|
|
118
|
+
elements.append(f'<link rel="icon" href="{self._escape(self.favicon)}">')
|
|
119
|
+
|
|
120
|
+
for meta in self.extra_meta:
|
|
121
|
+
attrs_str = " ".join(f'{k}="{self._escape(v)}"' for k, v in meta.items())
|
|
122
|
+
elements.append(f"<meta {attrs_str}>")
|
|
123
|
+
|
|
124
|
+
for link in self.extra_links:
|
|
125
|
+
attrs_str = " ".join(f'{k}="{self._escape(v)}"' for k, v in link.items())
|
|
126
|
+
elements.append(f"<link {attrs_str}>")
|
|
127
|
+
|
|
128
|
+
for script in self.extra_scripts:
|
|
129
|
+
attrs_str = " ".join(f'{k}="{self._escape(v)}"' for k, v in script.items())
|
|
130
|
+
if "src" in script:
|
|
131
|
+
elements.append(f"<script {attrs_str}></script>")
|
|
132
|
+
else:
|
|
133
|
+
content = script.pop("content", "")
|
|
134
|
+
elements.append(f"<script {attrs_str}>{content}</script>")
|
|
135
|
+
|
|
136
|
+
return "\n ".join(elements)
|
|
137
|
+
|
|
138
|
+
def __html__(self) -> str:
|
|
139
|
+
"""Make the component work with Jinja2's autoescape"""
|
|
140
|
+
return self.render()
|
|
141
|
+
|
|
142
|
+
def __str__(self) -> str:
|
|
143
|
+
return self.render()
|
|
144
|
+
|
|
145
|
+
def __call__(self, **kwargs: Any) -> Markup:
|
|
146
|
+
"""Allow calling Head() in templates with additional args"""
|
|
147
|
+
for key, value in kwargs.items():
|
|
148
|
+
if hasattr(self, key):
|
|
149
|
+
setattr(self, key, value)
|
|
150
|
+
return Markup(self.render())
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _escape(value: str) -> str:
|
|
154
|
+
"""Escape HTML special characters"""
|
|
155
|
+
return (
|
|
156
|
+
str(value)
|
|
157
|
+
.replace("&", "&")
|
|
158
|
+
.replace("<", "<")
|
|
159
|
+
.replace(">", ">")
|
|
160
|
+
.replace('"', """)
|
|
161
|
+
.replace("'", "'")
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def create_head(**kwargs: Any) -> Head:
|
|
166
|
+
"""Factory function to create a Head component"""
|
|
167
|
+
return Head(**kwargs)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hooks Provider for SSR Integration
|
|
3
|
+
Enables hooks to work seamlessly in server-side rendered pages
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class HooksContext:
|
|
12
|
+
"""Context for hooks across requests"""
|
|
13
|
+
component_id: str
|
|
14
|
+
state_data: Dict[str, Any]
|
|
15
|
+
request_id: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HooksProvider:
|
|
19
|
+
"""Global hooks provider for SSR"""
|
|
20
|
+
|
|
21
|
+
_instance = None
|
|
22
|
+
_request_contexts: Dict[str, HooksContext] = {}
|
|
23
|
+
|
|
24
|
+
def __new__(cls):
|
|
25
|
+
if cls._instance is None:
|
|
26
|
+
cls._instance = super().__new__(cls)
|
|
27
|
+
return cls._instance
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def create_context(cls, component_id: str, request_id: Optional[str] = None) -> HooksContext:
|
|
31
|
+
"""Create new hooks context for a request"""
|
|
32
|
+
context = HooksContext(
|
|
33
|
+
component_id=component_id,
|
|
34
|
+
state_data={},
|
|
35
|
+
request_id=request_id
|
|
36
|
+
)
|
|
37
|
+
if request_id:
|
|
38
|
+
cls._request_contexts[request_id] = context
|
|
39
|
+
return context
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_context(cls, request_id: str) -> Optional[HooksContext]:
|
|
43
|
+
"""Get hooks context for a request"""
|
|
44
|
+
return cls._request_contexts.get(request_id)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def cleanup_request(cls, request_id: str):
|
|
48
|
+
"""Clean up context after request"""
|
|
49
|
+
if request_id in cls._request_contexts:
|
|
50
|
+
del cls._request_contexts[request_id]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def with_hooks(component_id: str):
|
|
54
|
+
"""Decorator to enable hooks in page components"""
|
|
55
|
+
def decorator(func):
|
|
56
|
+
def wrapper(*args, **kwargs):
|
|
57
|
+
from nextpy.hooks import StateManager
|
|
58
|
+
StateManager.set_component(component_id)
|
|
59
|
+
try:
|
|
60
|
+
return func(*args, **kwargs)
|
|
61
|
+
finally:
|
|
62
|
+
StateManager.reset_hook_index()
|
|
63
|
+
return wrapper
|
|
64
|
+
return decorator
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NextPy Image Component - Optimized image handling
|
|
3
|
+
Similar to Next.js's next/image with automatic optimization
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, List, Optional, Tuple
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from markupsafe import Markup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Image:
|
|
12
|
+
"""
|
|
13
|
+
Optimized Image component with automatic sizing and lazy loading
|
|
14
|
+
|
|
15
|
+
Usage in templates:
|
|
16
|
+
{{ Image("/images/hero.jpg", alt="Hero image", width=800, height=600) }}
|
|
17
|
+
|
|
18
|
+
Features:
|
|
19
|
+
- Automatic srcset generation for responsive images
|
|
20
|
+
- Lazy loading by default
|
|
21
|
+
- Placeholder blur support
|
|
22
|
+
- Size optimization hints
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
DEFAULT_SIZES = [640, 750, 828, 1080, 1200, 1920, 2048, 3840]
|
|
26
|
+
DEFAULT_QUALITY = 75
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
src: str,
|
|
31
|
+
alt: str = "",
|
|
32
|
+
width: Optional[int] = None,
|
|
33
|
+
height: Optional[int] = None,
|
|
34
|
+
layout: str = "intrinsic",
|
|
35
|
+
priority: bool = False,
|
|
36
|
+
placeholder: str = "empty",
|
|
37
|
+
blur_data_url: Optional[str] = None,
|
|
38
|
+
quality: int = DEFAULT_QUALITY,
|
|
39
|
+
sizes: Optional[str] = None,
|
|
40
|
+
class_: Optional[str] = None,
|
|
41
|
+
style: Optional[str] = None,
|
|
42
|
+
**kwargs: Any,
|
|
43
|
+
):
|
|
44
|
+
self.src = src
|
|
45
|
+
self.alt = alt
|
|
46
|
+
self.width = width
|
|
47
|
+
self.height = height
|
|
48
|
+
self.layout = layout
|
|
49
|
+
self.priority = priority
|
|
50
|
+
self.placeholder = placeholder
|
|
51
|
+
self.blur_data_url = blur_data_url
|
|
52
|
+
self.quality = quality
|
|
53
|
+
self.sizes = sizes
|
|
54
|
+
self.class_ = class_
|
|
55
|
+
self.style = style
|
|
56
|
+
self.extra_attrs = kwargs
|
|
57
|
+
|
|
58
|
+
def render(self) -> str:
|
|
59
|
+
"""Render the image as an optimized HTML img tag"""
|
|
60
|
+
attrs = []
|
|
61
|
+
|
|
62
|
+
if self._is_external_url():
|
|
63
|
+
attrs.append(f'src="{self._escape(self.src)}"')
|
|
64
|
+
else:
|
|
65
|
+
attrs.append(f'src="{self._escape(self._get_optimized_url())}"')
|
|
66
|
+
srcset = self._generate_srcset()
|
|
67
|
+
if srcset:
|
|
68
|
+
attrs.append(f'srcset="{srcset}"')
|
|
69
|
+
|
|
70
|
+
attrs.append(f'alt="{self._escape(self.alt)}"')
|
|
71
|
+
|
|
72
|
+
if self.width:
|
|
73
|
+
attrs.append(f'width="{self.width}"')
|
|
74
|
+
if self.height:
|
|
75
|
+
attrs.append(f'height="{self.height}"')
|
|
76
|
+
|
|
77
|
+
if self.sizes:
|
|
78
|
+
attrs.append(f'sizes="{self._escape(self.sizes)}"')
|
|
79
|
+
elif self.width:
|
|
80
|
+
attrs.append(f'sizes="(max-width: {self.width}px) 100vw, {self.width}px"')
|
|
81
|
+
|
|
82
|
+
if not self.priority:
|
|
83
|
+
attrs.append('loading="lazy"')
|
|
84
|
+
attrs.append('decoding="async"')
|
|
85
|
+
else:
|
|
86
|
+
attrs.append('fetchpriority="high"')
|
|
87
|
+
|
|
88
|
+
if self.placeholder == "blur" and self.blur_data_url:
|
|
89
|
+
attrs.append(f'style="background-image: url({self.blur_data_url}); background-size: cover;"')
|
|
90
|
+
elif self.style:
|
|
91
|
+
attrs.append(f'style="{self._escape(self.style)}"')
|
|
92
|
+
|
|
93
|
+
if self.class_:
|
|
94
|
+
attrs.append(f'class="{self._escape(self.class_)}"')
|
|
95
|
+
|
|
96
|
+
for key, value in self.extra_attrs.items():
|
|
97
|
+
attr_name = key.replace("_", "-")
|
|
98
|
+
if value is True:
|
|
99
|
+
attrs.append(attr_name)
|
|
100
|
+
elif value is not False and value is not None:
|
|
101
|
+
attrs.append(f'{attr_name}="{self._escape(str(value))}"')
|
|
102
|
+
|
|
103
|
+
wrapper_style = self._get_wrapper_style()
|
|
104
|
+
img_tag = f"<img {' '.join(attrs)}>"
|
|
105
|
+
|
|
106
|
+
if wrapper_style:
|
|
107
|
+
return f'<span style="{wrapper_style}">{img_tag}</span>'
|
|
108
|
+
return img_tag
|
|
109
|
+
|
|
110
|
+
def _is_external_url(self) -> bool:
|
|
111
|
+
"""Check if the src is an external URL"""
|
|
112
|
+
return self.src.startswith(("http://", "https://", "//"))
|
|
113
|
+
|
|
114
|
+
def _get_optimized_url(self) -> str:
|
|
115
|
+
"""Get the optimized image URL"""
|
|
116
|
+
return f"/_nextpy/image?url={self.src}&w={self.width or 0}&q={self.quality}"
|
|
117
|
+
|
|
118
|
+
def _generate_srcset(self) -> str:
|
|
119
|
+
"""Generate srcset for responsive images"""
|
|
120
|
+
if self._is_external_url():
|
|
121
|
+
return ""
|
|
122
|
+
|
|
123
|
+
srcset_parts = []
|
|
124
|
+
max_width = self.width or 1920
|
|
125
|
+
|
|
126
|
+
for size in self.DEFAULT_SIZES:
|
|
127
|
+
if size <= max_width * 2:
|
|
128
|
+
url = f"/_nextpy/image?url={self.src}&w={size}&q={self.quality}"
|
|
129
|
+
srcset_parts.append(f"{url} {size}w")
|
|
130
|
+
|
|
131
|
+
return ", ".join(srcset_parts)
|
|
132
|
+
|
|
133
|
+
def _get_wrapper_style(self) -> str:
|
|
134
|
+
"""Get wrapper style based on layout mode"""
|
|
135
|
+
if self.layout == "fill":
|
|
136
|
+
return "display: block; overflow: hidden; position: absolute; inset: 0;"
|
|
137
|
+
elif self.layout == "responsive" and self.width and self.height:
|
|
138
|
+
aspect = (self.height / self.width) * 100
|
|
139
|
+
return f"display: block; overflow: hidden; position: relative; padding-bottom: {aspect:.2f}%;"
|
|
140
|
+
elif self.layout == "intrinsic" and self.width and self.height:
|
|
141
|
+
return f"display: inline-block; max-width: 100%; width: {self.width}px;"
|
|
142
|
+
return ""
|
|
143
|
+
|
|
144
|
+
def __html__(self) -> str:
|
|
145
|
+
"""Make the component work with Jinja2's autoescape"""
|
|
146
|
+
return self.render()
|
|
147
|
+
|
|
148
|
+
def __str__(self) -> str:
|
|
149
|
+
return self.render()
|
|
150
|
+
|
|
151
|
+
def __call__(self, **kwargs: Any) -> Markup:
|
|
152
|
+
"""Allow calling Image() in templates with arguments"""
|
|
153
|
+
for key, value in kwargs.items():
|
|
154
|
+
if hasattr(self, key):
|
|
155
|
+
setattr(self, key, value)
|
|
156
|
+
else:
|
|
157
|
+
self.extra_attrs[key] = value
|
|
158
|
+
return Markup(self.render())
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _escape(value: str) -> str:
|
|
162
|
+
"""Escape HTML special characters"""
|
|
163
|
+
return (
|
|
164
|
+
str(value)
|
|
165
|
+
.replace("&", "&")
|
|
166
|
+
.replace("<", "<")
|
|
167
|
+
.replace(">", ">")
|
|
168
|
+
.replace('"', """)
|
|
169
|
+
.replace("'", "'")
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def image(src: str, alt: str = "", **kwargs: Any) -> Markup:
|
|
174
|
+
"""
|
|
175
|
+
Functional helper to create an Image
|
|
176
|
+
|
|
177
|
+
Usage in templates:
|
|
178
|
+
{{ image("/hero.jpg", "Hero", width=800) }}
|
|
179
|
+
"""
|
|
180
|
+
return Markup(Image(src, alt, **kwargs).render())
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Layout components for NextPy
|
|
3
|
+
Provides grid, flex, container, and other layout utilities
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional, List, Dict, Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def Container(
|
|
10
|
+
children: str = "",
|
|
11
|
+
max_width: str = "max-w-6xl",
|
|
12
|
+
padding: str = "px-4 sm:px-6 lg:px-8",
|
|
13
|
+
**kwargs
|
|
14
|
+
) -> str:
|
|
15
|
+
"""Container component with responsive max-width"""
|
|
16
|
+
classes = f"mx-auto {max_width} {padding}"
|
|
17
|
+
return f'<div class="{classes}">{children}</div>'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def Grid(
|
|
21
|
+
children: str = "",
|
|
22
|
+
columns: int = 3,
|
|
23
|
+
gap: str = "gap-6",
|
|
24
|
+
responsive: bool = True,
|
|
25
|
+
**kwargs
|
|
26
|
+
) -> str:
|
|
27
|
+
"""Grid layout component"""
|
|
28
|
+
if responsive:
|
|
29
|
+
cols = f"md:grid-cols-{columns}"
|
|
30
|
+
classes = f"grid {cols} {gap}"
|
|
31
|
+
else:
|
|
32
|
+
cols = f"grid-cols-{columns}"
|
|
33
|
+
classes = f"grid {cols} {gap}"
|
|
34
|
+
|
|
35
|
+
return f'<div class="{classes}">{children}</div>'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def Flex(
|
|
39
|
+
children: str = "",
|
|
40
|
+
direction: str = "row",
|
|
41
|
+
justify: str = "justify-start",
|
|
42
|
+
align: str = "items-start",
|
|
43
|
+
gap: str = "gap-4",
|
|
44
|
+
**kwargs
|
|
45
|
+
) -> str:
|
|
46
|
+
"""Flex layout component"""
|
|
47
|
+
flex_dir = "flex-col" if direction == "column" else "flex-row"
|
|
48
|
+
classes = f"flex {flex_dir} {justify} {align} {gap}"
|
|
49
|
+
return f'<div class="{classes}">{children}</div>'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def Stack(
|
|
53
|
+
children: str = "",
|
|
54
|
+
direction: str = "vertical",
|
|
55
|
+
spacing: str = "space-y-4",
|
|
56
|
+
**kwargs
|
|
57
|
+
) -> str:
|
|
58
|
+
"""Stack component (vertical or horizontal)"""
|
|
59
|
+
if direction == "horizontal":
|
|
60
|
+
spacing = spacing.replace("space-y", "space-x")
|
|
61
|
+
classes = f"flex flex-row {spacing}"
|
|
62
|
+
else:
|
|
63
|
+
classes = f"flex flex-col {spacing}"
|
|
64
|
+
|
|
65
|
+
return f'<div class="{classes}">{children}</div>'
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def Section(
|
|
69
|
+
children: str = "",
|
|
70
|
+
title: Optional[str] = None,
|
|
71
|
+
bg_color: str = "bg-white",
|
|
72
|
+
padding: str = "py-16",
|
|
73
|
+
**kwargs
|
|
74
|
+
) -> str:
|
|
75
|
+
"""Section component with optional title"""
|
|
76
|
+
html = f'<section class="{bg_color} {padding}"><div class="max-w-6xl mx-auto px-4">'
|
|
77
|
+
|
|
78
|
+
if title:
|
|
79
|
+
html += f'<h2 class="text-3xl font-bold mb-8">{title}</h2>'
|
|
80
|
+
|
|
81
|
+
html += children + '</div></section>'
|
|
82
|
+
return html
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def Row(
|
|
86
|
+
children: str = "",
|
|
87
|
+
gap: str = "gap-4",
|
|
88
|
+
**kwargs
|
|
89
|
+
) -> str:
|
|
90
|
+
"""Row component (horizontal flex)"""
|
|
91
|
+
return f'<div class="flex flex-row {gap}">{children}</div>'
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def Column(
|
|
95
|
+
children: str = "",
|
|
96
|
+
gap: str = "gap-4",
|
|
97
|
+
**kwargs
|
|
98
|
+
) -> str:
|
|
99
|
+
"""Column component (vertical flex)"""
|
|
100
|
+
return f'<div class="flex flex-col {gap}">{children}</div>'
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def AspectRatio(
|
|
104
|
+
children: str = "",
|
|
105
|
+
ratio: str = "16/9",
|
|
106
|
+
**kwargs
|
|
107
|
+
) -> str:
|
|
108
|
+
"""AspectRatio component"""
|
|
109
|
+
ratio_class = {
|
|
110
|
+
"1/1": "aspect-square",
|
|
111
|
+
"4/3": "aspect-video",
|
|
112
|
+
"16/9": "aspect-video",
|
|
113
|
+
"3/2": "aspect-video",
|
|
114
|
+
}.get(ratio, "aspect-video")
|
|
115
|
+
|
|
116
|
+
return f'<div class="{ratio_class} overflow-hidden">{children}</div>'
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def Spacer(height: str = "h-4", **kwargs) -> str:
|
|
120
|
+
"""Spacer component for vertical spacing"""
|
|
121
|
+
return f'<div class="{height}"></div>'
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def Divider(
|
|
125
|
+
color: str = "border-gray-200",
|
|
126
|
+
**kwargs
|
|
127
|
+
) -> str:
|
|
128
|
+
"""Divider/separator component"""
|
|
129
|
+
return f'<hr class="border {color}">'
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def Center(
|
|
133
|
+
children: str = "",
|
|
134
|
+
**kwargs
|
|
135
|
+
) -> str:
|
|
136
|
+
"""Center component"""
|
|
137
|
+
return f'<div class="flex items-center justify-center">{children}</div>'
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def Sidebar(
|
|
141
|
+
children: str = "",
|
|
142
|
+
sidebar_content: str = "",
|
|
143
|
+
sidebar_width: str = "w-64",
|
|
144
|
+
**kwargs
|
|
145
|
+
) -> str:
|
|
146
|
+
"""Sidebar layout component"""
|
|
147
|
+
return f'''
|
|
148
|
+
<div class="flex gap-6">
|
|
149
|
+
<aside class="{sidebar_width} flex-shrink-0">
|
|
150
|
+
{sidebar_content}
|
|
151
|
+
</aside>
|
|
152
|
+
<main class="flex-1">
|
|
153
|
+
{children}
|
|
154
|
+
</main>
|
|
155
|
+
</div>
|
|
156
|
+
'''
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def TwoColumn(
|
|
160
|
+
left: str = "",
|
|
161
|
+
right: str = "",
|
|
162
|
+
gap: str = "gap-8",
|
|
163
|
+
**kwargs
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Two-column layout"""
|
|
166
|
+
return f'''
|
|
167
|
+
<div class="grid grid-cols-2 {gap}">
|
|
168
|
+
<div>{left}</div>
|
|
169
|
+
<div>{right}</div>
|
|
170
|
+
</div>
|
|
171
|
+
'''
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def ThreeColumn(
|
|
175
|
+
left: str = "",
|
|
176
|
+
center: str = "",
|
|
177
|
+
right: str = "",
|
|
178
|
+
gap: str = "gap-6",
|
|
179
|
+
**kwargs
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Three-column layout"""
|
|
182
|
+
return f'''
|
|
183
|
+
<div class="grid grid-cols-3 {gap}">
|
|
184
|
+
<div>{left}</div>
|
|
185
|
+
<div>{center}</div>
|
|
186
|
+
<div>{right}</div>
|
|
187
|
+
</div>
|
|
188
|
+
'''
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = [
|
|
192
|
+
'Container',
|
|
193
|
+
'Grid',
|
|
194
|
+
'Flex',
|
|
195
|
+
'Stack',
|
|
196
|
+
'Section',
|
|
197
|
+
'Row',
|
|
198
|
+
'Column',
|
|
199
|
+
'AspectRatio',
|
|
200
|
+
'Spacer',
|
|
201
|
+
'Divider',
|
|
202
|
+
'Center',
|
|
203
|
+
'Sidebar',
|
|
204
|
+
'TwoColumn',
|
|
205
|
+
'ThreeColumn',
|
|
206
|
+
]
|