eidosui 0.4.0__py3-none-any.whl → 0.6.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.
- eidos/__init__.py +270 -0
- eidos/components/__init__.py +11 -0
- eidos/components/headers.py +16 -14
- eidos/components/navigation.py +44 -35
- eidos/components/table.py +84 -0
- eidos/components/tabs.py +140 -0
- eidos/css/styles.css +155 -0
- eidos/plugins/__init__.py +1 -1
- eidos/plugins/markdown/__init__.py +3 -3
- eidos/plugins/markdown/components.py +10 -22
- eidos/plugins/markdown/extensions/__init__.py +1 -1
- eidos/plugins/markdown/extensions/alerts.py +68 -78
- eidos/plugins/markdown/renderer.py +19 -24
- eidos/styles.py +58 -11
- eidos/tags.py +144 -86
- eidos/utils.py +42 -38
- eidosui-0.6.0.dist-info/METADATA +113 -0
- eidosui-0.6.0.dist-info/RECORD +25 -0
- eidosui-0.4.0.dist-info/METADATA +0 -127
- eidosui-0.4.0.dist-info/RECORD +0 -22
- {eidosui-0.4.0.dist-info → eidosui-0.6.0.dist-info}/WHEEL +0 -0
- {eidosui-0.4.0.dist-info → eidosui-0.6.0.dist-info}/licenses/LICENSE +0 -0
eidos/__init__.py
CHANGED
@@ -0,0 +1,270 @@
|
|
1
|
+
"""EidosUI - A modern, flexible Tailwind CSS-based UI library for Python web frameworks.
|
2
|
+
|
3
|
+
Quick start:
|
4
|
+
>>> from eidos import *
|
5
|
+
>>> DataTable.from_lists([["A", "B"], ["C", "D"]], headers=["Col1", "Col2"])
|
6
|
+
|
7
|
+
Or use explicit imports:
|
8
|
+
>>> from eidos import DataTable, Button, H1, Table
|
9
|
+
>>> from eidos import styles
|
10
|
+
"""
|
11
|
+
|
12
|
+
# Import all styled HTML tags
|
13
|
+
# Import style namespaces
|
14
|
+
from . import styles
|
15
|
+
|
16
|
+
# Import components
|
17
|
+
from .components import DataTable, EidosHeaders, NavBar
|
18
|
+
from .styles import buttons, lists, tables, typography
|
19
|
+
from .tags import (
|
20
|
+
# Headings
|
21
|
+
H1,
|
22
|
+
H2,
|
23
|
+
H3,
|
24
|
+
H4,
|
25
|
+
H5,
|
26
|
+
H6,
|
27
|
+
# Pass-through HTML tags
|
28
|
+
A,
|
29
|
+
Abbr,
|
30
|
+
Address,
|
31
|
+
Area,
|
32
|
+
Article,
|
33
|
+
Aside,
|
34
|
+
Audio,
|
35
|
+
B,
|
36
|
+
Base,
|
37
|
+
Bdi,
|
38
|
+
Bdo,
|
39
|
+
Blockquote,
|
40
|
+
# Body
|
41
|
+
Body,
|
42
|
+
Br,
|
43
|
+
# Buttons
|
44
|
+
Button,
|
45
|
+
Canvas,
|
46
|
+
Caption,
|
47
|
+
Cite,
|
48
|
+
Code,
|
49
|
+
Col,
|
50
|
+
Colgroup,
|
51
|
+
Data,
|
52
|
+
Datalist,
|
53
|
+
Dd,
|
54
|
+
Del,
|
55
|
+
Details,
|
56
|
+
Dfn,
|
57
|
+
Dialog,
|
58
|
+
Div,
|
59
|
+
Dl,
|
60
|
+
Dt,
|
61
|
+
Em,
|
62
|
+
Embed,
|
63
|
+
Fieldset,
|
64
|
+
Figcaption,
|
65
|
+
Figure,
|
66
|
+
Footer,
|
67
|
+
Form,
|
68
|
+
Head,
|
69
|
+
Header,
|
70
|
+
Hgroup,
|
71
|
+
Hr,
|
72
|
+
Html,
|
73
|
+
I,
|
74
|
+
Iframe,
|
75
|
+
Img,
|
76
|
+
Input,
|
77
|
+
Ins,
|
78
|
+
Kbd,
|
79
|
+
Label,
|
80
|
+
Legend,
|
81
|
+
Li,
|
82
|
+
Link,
|
83
|
+
Main,
|
84
|
+
Map,
|
85
|
+
Mark,
|
86
|
+
Menu,
|
87
|
+
Meta,
|
88
|
+
Meter,
|
89
|
+
Nav,
|
90
|
+
Noscript,
|
91
|
+
Object,
|
92
|
+
Ol,
|
93
|
+
Optgroup,
|
94
|
+
Option,
|
95
|
+
Output,
|
96
|
+
P,
|
97
|
+
Param,
|
98
|
+
Picture,
|
99
|
+
Pre,
|
100
|
+
Progress,
|
101
|
+
Q,
|
102
|
+
Rp,
|
103
|
+
Rt,
|
104
|
+
Ruby,
|
105
|
+
S,
|
106
|
+
Samp,
|
107
|
+
Script,
|
108
|
+
Search,
|
109
|
+
Section,
|
110
|
+
Select,
|
111
|
+
Small,
|
112
|
+
Source,
|
113
|
+
Span,
|
114
|
+
# Semantic typography
|
115
|
+
Strong,
|
116
|
+
Style,
|
117
|
+
Sub,
|
118
|
+
Summary,
|
119
|
+
Sup,
|
120
|
+
# Table elements
|
121
|
+
Table,
|
122
|
+
Tbody,
|
123
|
+
Td,
|
124
|
+
Template,
|
125
|
+
Textarea,
|
126
|
+
Tfoot,
|
127
|
+
Th,
|
128
|
+
Thead,
|
129
|
+
Time,
|
130
|
+
Title,
|
131
|
+
Tr,
|
132
|
+
Track,
|
133
|
+
U,
|
134
|
+
Ul,
|
135
|
+
Var,
|
136
|
+
Video,
|
137
|
+
Wbr,
|
138
|
+
)
|
139
|
+
|
140
|
+
# Version info
|
141
|
+
__version__ = "0.4.0"
|
142
|
+
|
143
|
+
# Define what's available with "from eidos import *"
|
144
|
+
__all__ = [
|
145
|
+
# Version
|
146
|
+
"__version__",
|
147
|
+
# Style namespaces
|
148
|
+
"styles",
|
149
|
+
"buttons",
|
150
|
+
"typography",
|
151
|
+
"lists",
|
152
|
+
"tables",
|
153
|
+
# Components
|
154
|
+
"DataTable",
|
155
|
+
"NavBar",
|
156
|
+
"EidosHeaders",
|
157
|
+
# HTML Tags
|
158
|
+
"H1",
|
159
|
+
"H2",
|
160
|
+
"H3",
|
161
|
+
"H4",
|
162
|
+
"H5",
|
163
|
+
"H6",
|
164
|
+
"Body",
|
165
|
+
"Button",
|
166
|
+
"Strong",
|
167
|
+
"I",
|
168
|
+
"Small",
|
169
|
+
"Del",
|
170
|
+
"Abbr",
|
171
|
+
"Var",
|
172
|
+
"Mark",
|
173
|
+
"Time",
|
174
|
+
"Code",
|
175
|
+
"Pre",
|
176
|
+
"Kbd",
|
177
|
+
"Samp",
|
178
|
+
"Blockquote",
|
179
|
+
"Cite",
|
180
|
+
"Address",
|
181
|
+
"Hr",
|
182
|
+
"Details",
|
183
|
+
"Summary",
|
184
|
+
"Dl",
|
185
|
+
"Dt",
|
186
|
+
"Dd",
|
187
|
+
"Figure",
|
188
|
+
"Figcaption",
|
189
|
+
"Table",
|
190
|
+
"Thead",
|
191
|
+
"Tbody",
|
192
|
+
"Tfoot",
|
193
|
+
"Tr",
|
194
|
+
"Th",
|
195
|
+
"Td",
|
196
|
+
"A",
|
197
|
+
"Area",
|
198
|
+
"Article",
|
199
|
+
"Aside",
|
200
|
+
"Audio",
|
201
|
+
"B",
|
202
|
+
"Base",
|
203
|
+
"Bdi",
|
204
|
+
"Bdo",
|
205
|
+
"Br",
|
206
|
+
"Canvas",
|
207
|
+
"Caption",
|
208
|
+
"Col",
|
209
|
+
"Colgroup",
|
210
|
+
"Data",
|
211
|
+
"Datalist",
|
212
|
+
"Dfn",
|
213
|
+
"Dialog",
|
214
|
+
"Div",
|
215
|
+
"Em",
|
216
|
+
"Embed",
|
217
|
+
"Fieldset",
|
218
|
+
"Footer",
|
219
|
+
"Form",
|
220
|
+
"Head",
|
221
|
+
"Header",
|
222
|
+
"Hgroup",
|
223
|
+
"Html",
|
224
|
+
"Iframe",
|
225
|
+
"Img",
|
226
|
+
"Input",
|
227
|
+
"Ins",
|
228
|
+
"Label",
|
229
|
+
"Legend",
|
230
|
+
"Li",
|
231
|
+
"Link",
|
232
|
+
"Main",
|
233
|
+
"Map",
|
234
|
+
"Menu",
|
235
|
+
"Meta",
|
236
|
+
"Meter",
|
237
|
+
"Nav",
|
238
|
+
"Noscript",
|
239
|
+
"Object",
|
240
|
+
"Ol",
|
241
|
+
"Optgroup",
|
242
|
+
"Option",
|
243
|
+
"Output",
|
244
|
+
"P",
|
245
|
+
"Param",
|
246
|
+
"Picture",
|
247
|
+
"Progress",
|
248
|
+
"Q",
|
249
|
+
"Rp",
|
250
|
+
"Rt",
|
251
|
+
"Ruby",
|
252
|
+
"S",
|
253
|
+
"Script",
|
254
|
+
"Search",
|
255
|
+
"Section",
|
256
|
+
"Select",
|
257
|
+
"Source",
|
258
|
+
"Span",
|
259
|
+
"Style",
|
260
|
+
"Sub",
|
261
|
+
"Sup",
|
262
|
+
"Template",
|
263
|
+
"Textarea",
|
264
|
+
"Title",
|
265
|
+
"Track",
|
266
|
+
"U",
|
267
|
+
"Ul",
|
268
|
+
"Video",
|
269
|
+
"Wbr",
|
270
|
+
]
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"""EidosUI Components Package
|
2
|
+
|
3
|
+
Higher-level components built on top of the base tags.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from .headers import EidosHeaders
|
7
|
+
from .navigation import NavBar
|
8
|
+
from .table import DataTable
|
9
|
+
from .tabs import TabContainer, TabList, TabPanel, Tabs
|
10
|
+
|
11
|
+
__all__ = ["DataTable", "NavBar", "EidosHeaders", "TabContainer", "TabList", "TabPanel", "Tabs"]
|
eidos/components/headers.py
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
-
from air import Meta, Script, Link
|
2
|
-
from ..tags import Body
|
3
1
|
from typing import Literal
|
4
2
|
|
3
|
+
from air import Link, Meta, Script
|
4
|
+
|
5
|
+
|
5
6
|
def get_css_urls():
|
6
7
|
"""Return list of CSS URLs for EidosUI."""
|
7
8
|
return [
|
8
9
|
"/eidos/css/styles.css",
|
9
|
-
"/eidos/css/themes/eidos-variables.css",
|
10
|
+
"/eidos/css/themes/eidos-variables.css",
|
10
11
|
"/eidos/css/themes/light.css",
|
11
|
-
"/eidos/css/themes/dark.css"
|
12
|
+
"/eidos/css/themes/dark.css",
|
12
13
|
]
|
13
14
|
|
15
|
+
|
14
16
|
def EidosHeaders(
|
15
17
|
include_tailwind: bool = True,
|
16
18
|
include_lucide: bool = True,
|
@@ -18,7 +20,7 @@ def EidosHeaders(
|
|
18
20
|
theme: Literal["light", "dark"] = "light",
|
19
21
|
):
|
20
22
|
"""Complete EidosUI headers with EidosUI JavaScript support.
|
21
|
-
|
23
|
+
|
22
24
|
Args:
|
23
25
|
include_tailwind: Include Tailwind CSS CDN
|
24
26
|
include_lucide: Include Lucide Icons CDN
|
@@ -29,28 +31,28 @@ def EidosHeaders(
|
|
29
31
|
Meta(charset="UTF-8"),
|
30
32
|
Meta(name="viewport", content="width=device-width, initial-scale=1.0"),
|
31
33
|
]
|
32
|
-
|
34
|
+
|
33
35
|
# Core libraries
|
34
36
|
if include_tailwind:
|
35
37
|
headers.append(Script(src="https://cdn.tailwindcss.com"))
|
36
|
-
|
38
|
+
|
37
39
|
if include_lucide:
|
38
40
|
headers.append(Script(src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"))
|
39
|
-
|
41
|
+
|
40
42
|
# EidosUI CSS
|
41
43
|
for css_url in get_css_urls():
|
42
44
|
headers.append(Link(rel="stylesheet", href=css_url))
|
43
|
-
|
45
|
+
|
44
46
|
# EidosUI JavaScript
|
45
47
|
if include_eidos_js:
|
46
48
|
headers.append(Script(src="/eidos/js/eidos.js", defer=True))
|
47
|
-
|
49
|
+
|
48
50
|
# Initialization script
|
49
51
|
init_script = f"""
|
50
52
|
// Set theme
|
51
53
|
document.documentElement.setAttribute('data-theme', '{theme}');
|
52
54
|
"""
|
53
|
-
|
55
|
+
|
54
56
|
if include_lucide:
|
55
57
|
init_script += """
|
56
58
|
// Initialize Lucide icons
|
@@ -62,7 +64,7 @@ def EidosHeaders(
|
|
62
64
|
if (window.lucide) lucide.createIcons();
|
63
65
|
}
|
64
66
|
"""
|
65
|
-
|
67
|
+
|
66
68
|
headers.append(Script(init_script))
|
67
|
-
|
68
|
-
return headers
|
69
|
+
|
70
|
+
return headers
|
eidos/components/navigation.py
CHANGED
@@ -1,35 +1,44 @@
|
|
1
|
-
from
|
1
|
+
from typing import Any, Final
|
2
|
+
from uuid import uuid4
|
3
|
+
|
4
|
+
from air import A, Div, I, Tag
|
5
|
+
|
2
6
|
from ..tags import *
|
3
7
|
from ..utils import stringify
|
4
|
-
|
5
|
-
from uuid import uuid4
|
8
|
+
|
6
9
|
|
7
10
|
class ScrollspyT:
|
8
|
-
underline: Final[str] =
|
9
|
-
bold: Final[str] =
|
11
|
+
underline: Final[str] = "navbar-underline"
|
12
|
+
bold: Final[str] = "navbar-bold"
|
13
|
+
|
10
14
|
|
11
|
-
def NavBar(
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
def NavBar(
|
16
|
+
*c: Any,
|
17
|
+
lcontents: Tag | None = None,
|
18
|
+
right_cls: str = "items-center space-x-4",
|
19
|
+
mobile_cls: str = "",
|
20
|
+
sticky: bool = False,
|
21
|
+
scrollspy: bool = False,
|
22
|
+
cls: str = "p-4",
|
23
|
+
scrollspy_cls: str = ScrollspyT.underline,
|
24
|
+
menu_id: str | None = None,
|
25
|
+
) -> Tag:
|
21
26
|
"""Pure Tailwind responsive navigation bar with optional scrollspy.
|
22
|
-
|
27
|
+
|
23
28
|
Mobile menu uses best practice dropdown with:
|
24
29
|
- Centered text links
|
25
30
|
- Large touch targets
|
26
31
|
- Auto-close on selection
|
27
32
|
- Smooth animations
|
28
33
|
"""
|
29
|
-
if
|
30
|
-
|
31
|
-
|
32
|
-
|
34
|
+
if lcontents is None:
|
35
|
+
lcontents = Div()
|
36
|
+
|
37
|
+
if menu_id is None:
|
38
|
+
menu_id = f"menu-{uuid4().hex[:8]}"
|
39
|
+
|
40
|
+
sticky_cls = "sticky top-0 eidos-navbar-sticky z-50" if sticky else ""
|
41
|
+
|
33
42
|
# Mobile toggle button with hamburger/close icon
|
34
43
|
mobile_icon = A(
|
35
44
|
I(data_lucide="menu", class_="w-6 h-6", data_menu_icon="open"),
|
@@ -38,30 +47,30 @@ def NavBar(*c: Any,
|
|
38
47
|
data_toggle=f"#{menu_id}",
|
39
48
|
role="button",
|
40
49
|
aria_label="Toggle navigation",
|
41
|
-
aria_expanded="false"
|
50
|
+
aria_expanded="false",
|
42
51
|
)
|
43
|
-
|
52
|
+
|
44
53
|
# Desktop navigation
|
45
54
|
desktop_nav = Div(
|
46
55
|
*c,
|
47
|
-
class_=stringify(right_cls,
|
48
|
-
data_scrollspy="true" if scrollspy else None
|
56
|
+
class_=stringify(right_cls, "hidden md:flex"),
|
57
|
+
data_scrollspy="true" if scrollspy else None,
|
49
58
|
)
|
50
|
-
|
59
|
+
|
51
60
|
# Mobile navigation
|
52
61
|
mobile_nav = Div(
|
53
62
|
*c,
|
54
63
|
class_=stringify(
|
55
|
-
mobile_cls,
|
56
|
-
|
57
|
-
|
58
|
-
scrollspy_cls
|
64
|
+
mobile_cls,
|
65
|
+
"hidden md:hidden absolute top-full left-0 right-0 eidos-navbar-mobile shadow-lg border-t",
|
66
|
+
"flex flex-col eidos-navbar-mobile-divider" if not mobile_cls else "",
|
67
|
+
scrollspy_cls,
|
59
68
|
),
|
60
69
|
id=menu_id,
|
61
70
|
data_scrollspy="true" if scrollspy else None,
|
62
|
-
data_mobile_menu="true"
|
71
|
+
data_mobile_menu="true",
|
63
72
|
)
|
64
|
-
|
73
|
+
|
65
74
|
return Div(
|
66
75
|
# Main navbar container with relative positioning for mobile dropdown
|
67
76
|
Div(
|
@@ -69,10 +78,10 @@ def NavBar(*c: Any,
|
|
69
78
|
lcontents,
|
70
79
|
mobile_icon,
|
71
80
|
desktop_nav,
|
72
|
-
class_=
|
81
|
+
class_="flex items-center justify-between",
|
73
82
|
),
|
74
83
|
mobile_nav,
|
75
|
-
class_=stringify(
|
84
|
+
class_=stringify("eidos-navbar relative", cls, scrollspy_cls),
|
76
85
|
),
|
77
|
-
class_=sticky_cls
|
78
|
-
)
|
86
|
+
class_=sticky_cls,
|
87
|
+
)
|
@@ -0,0 +1,84 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from ..tags import Table as BaseTable
|
4
|
+
from ..tags import Tbody, Td, Th, Thead, Tr
|
5
|
+
|
6
|
+
|
7
|
+
class DataTable:
|
8
|
+
"""DataTable component that provides convenient methods for creating tables from data."""
|
9
|
+
|
10
|
+
@classmethod
|
11
|
+
def from_lists(
|
12
|
+
cls,
|
13
|
+
data: list[list],
|
14
|
+
headers: list[str] | None = None,
|
15
|
+
class_: str | list[str] | None = None,
|
16
|
+
**kwargs: Any,
|
17
|
+
) -> Any:
|
18
|
+
"""Create table from list of lists.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
data: List of lists where each inner list is a row
|
22
|
+
headers: Optional list of header strings
|
23
|
+
class_: Optional CSS classes to add to the table
|
24
|
+
**kwargs: Additional attributes to pass to the table element
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
A rendered table element
|
28
|
+
|
29
|
+
Example:
|
30
|
+
Table.from_lists([["A", "B"], ["C", "D"]], headers=["Col1", "Col2"])
|
31
|
+
"""
|
32
|
+
content = []
|
33
|
+
|
34
|
+
if headers:
|
35
|
+
thead = Thead(Tr(*[Th(header) for header in headers]))
|
36
|
+
content.append(thead)
|
37
|
+
|
38
|
+
tbody_rows = []
|
39
|
+
for row_data in data:
|
40
|
+
tbody_rows.append(Tr(*[Td(cell) for cell in row_data]))
|
41
|
+
tbody = Tbody(*tbody_rows)
|
42
|
+
content.append(tbody)
|
43
|
+
|
44
|
+
return BaseTable(*content, class_=class_, **kwargs)
|
45
|
+
|
46
|
+
@classmethod
|
47
|
+
def from_dicts(
|
48
|
+
cls,
|
49
|
+
data: list[dict],
|
50
|
+
headers: list[str] | None = None,
|
51
|
+
class_: str | list[str] | None = None,
|
52
|
+
**kwargs: Any,
|
53
|
+
) -> Any:
|
54
|
+
"""Create table from list of dictionaries.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
data: List of dictionaries where each dict is a row
|
58
|
+
headers: Optional list of header strings. If not provided, uses keys from first dict
|
59
|
+
class_: Optional CSS classes to add to the table
|
60
|
+
**kwargs: Additional attributes to pass to the table element
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
A rendered table element
|
64
|
+
|
65
|
+
Example:
|
66
|
+
Table.from_dicts([{"name": "John", "age": 25}], headers=["name", "age"])
|
67
|
+
"""
|
68
|
+
if headers is None and data:
|
69
|
+
headers = list(data[0].keys())
|
70
|
+
|
71
|
+
content = []
|
72
|
+
|
73
|
+
if headers:
|
74
|
+
thead = Thead(Tr(*[Th(header) for header in headers]))
|
75
|
+
content.append(thead)
|
76
|
+
|
77
|
+
tbody_rows = []
|
78
|
+
if data and headers:
|
79
|
+
for row in data:
|
80
|
+
tbody_rows.append(Tr(*[Td(row.get(key, "")) for key in headers]))
|
81
|
+
tbody = Tbody(*tbody_rows)
|
82
|
+
content.append(tbody)
|
83
|
+
|
84
|
+
return BaseTable(*content, class_=class_, **kwargs)
|
eidos/components/tabs.py
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
from air import Button, Div, Tag
|
2
|
+
|
3
|
+
from .. import styles
|
4
|
+
from ..utils import stringify
|
5
|
+
|
6
|
+
|
7
|
+
def TabContainer(
|
8
|
+
initial_tab_url: str,
|
9
|
+
cls: str = "",
|
10
|
+
target_id: str = "tabs",
|
11
|
+
) -> Tag:
|
12
|
+
"""HTMX-based tab container that loads tabs dynamically.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
initial_tab_url: URL to load the initial tab content
|
16
|
+
cls: Additional classes for the container
|
17
|
+
target_id: ID for the tab container (default: "tabs")
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
Tag: The tab container that will be populated via HTMX
|
21
|
+
|
22
|
+
Example:
|
23
|
+
TabContainer("/settings/general")
|
24
|
+
"""
|
25
|
+
return Div(
|
26
|
+
id=target_id,
|
27
|
+
hx_get=initial_tab_url,
|
28
|
+
hx_trigger="load delay:100ms",
|
29
|
+
hx_target=f"#{target_id}",
|
30
|
+
hx_swap="innerHTML",
|
31
|
+
class_=stringify(styles.tabs.container, cls),
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
def TabList(
|
36
|
+
*tabs: tuple[str, str],
|
37
|
+
selected: int = 0,
|
38
|
+
tab_cls: str = "",
|
39
|
+
hx_target: str = "#tabs",
|
40
|
+
hx_swap: str = "innerHTML",
|
41
|
+
) -> Tag:
|
42
|
+
"""HTMX-based tab list for server-rendered tabs.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
*tabs: Variable number of (label, url) tuples
|
46
|
+
selected: Index of the selected tab (0-based)
|
47
|
+
tab_cls: Additional classes for tab buttons
|
48
|
+
hx_target: HTMX target for tab content (default: "#tabs")
|
49
|
+
hx_swap: HTMX swap method (default: "innerHTML")
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
Tag: The tab list component
|
53
|
+
|
54
|
+
Example:
|
55
|
+
TabList(
|
56
|
+
("General", "/settings/general"),
|
57
|
+
("Security", "/settings/security"),
|
58
|
+
("Advanced", "/settings/advanced"),
|
59
|
+
selected=0
|
60
|
+
)
|
61
|
+
"""
|
62
|
+
tab_buttons = []
|
63
|
+
|
64
|
+
for i, (label, url) in enumerate(tabs):
|
65
|
+
is_selected = i == selected
|
66
|
+
|
67
|
+
tab_button = Button(
|
68
|
+
label,
|
69
|
+
hx_get=url,
|
70
|
+
hx_target=hx_target,
|
71
|
+
hx_swap=hx_swap,
|
72
|
+
role="tab",
|
73
|
+
aria_selected="true" if is_selected else "false",
|
74
|
+
aria_controls="tab-content",
|
75
|
+
class_=stringify(
|
76
|
+
styles.tabs.tab,
|
77
|
+
styles.tabs.tab_active if is_selected else "",
|
78
|
+
tab_cls
|
79
|
+
),
|
80
|
+
)
|
81
|
+
tab_buttons.append(tab_button)
|
82
|
+
|
83
|
+
return Div(
|
84
|
+
*tab_buttons,
|
85
|
+
role="tablist",
|
86
|
+
class_=styles.tabs.list,
|
87
|
+
)
|
88
|
+
|
89
|
+
|
90
|
+
def TabPanel(
|
91
|
+
content: Tag,
|
92
|
+
panel_cls: str = "",
|
93
|
+
) -> Tag:
|
94
|
+
"""Tab panel content wrapper.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
content: The content to display in the tab panel
|
98
|
+
panel_cls: Additional classes for the panel
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
Tag: The tab panel component
|
102
|
+
"""
|
103
|
+
return Div(
|
104
|
+
content,
|
105
|
+
id="tab-content",
|
106
|
+
role="tabpanel",
|
107
|
+
class_=stringify(styles.tabs.panel, styles.tabs.panel_active, panel_cls),
|
108
|
+
)
|
109
|
+
|
110
|
+
|
111
|
+
def Tabs(
|
112
|
+
tab_list: Tag,
|
113
|
+
tab_panel: Tag,
|
114
|
+
cls: str = "",
|
115
|
+
) -> Tag:
|
116
|
+
"""Complete tab component with list and panel.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
tab_list: The TabList component
|
120
|
+
tab_panel: The TabPanel component
|
121
|
+
cls: Additional classes for the container
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
Tag: The complete tabs component
|
125
|
+
|
126
|
+
Example:
|
127
|
+
# In your route handler:
|
128
|
+
tab_list = TabList(
|
129
|
+
("General", "/settings/general"),
|
130
|
+
("Security", "/settings/security"),
|
131
|
+
selected=0
|
132
|
+
)
|
133
|
+
tab_panel = TabPanel(general_settings_content)
|
134
|
+
return Tabs(tab_list, tab_panel)
|
135
|
+
"""
|
136
|
+
return Div(
|
137
|
+
tab_list,
|
138
|
+
tab_panel,
|
139
|
+
class_=stringify(styles.tabs.container, cls),
|
140
|
+
)
|