pydzn 0.1.3__py3-none-any.whl → 0.1.4__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.
- pydzn/components/__init__.py +3 -1
- pydzn/components/button/component.py +74 -14
- pydzn/components/card/component.py +83 -5
- pydzn/components/hamburger_menu/component.py +210 -0
- pydzn/components/hamburger_menu/template.html +3 -0
- pydzn/components/nav_item/component.py +198 -34
- pydzn/components/sidebar/component.py +61 -6
- pydzn/dzn.py +96 -0
- pydzn/grid_builder.py +77 -18
- pydzn/responsive.py +24 -0
- pydzn/variants.py +215 -0
- pydzn-0.1.4.dist-info/METADATA +168 -0
- pydzn-0.1.4.dist-info/RECORD +27 -0
- pydzn-0.1.3.dist-info/METADATA +0 -38
- pydzn-0.1.3.dist-info/RECORD +0 -23
- {pydzn-0.1.3.dist-info → pydzn-0.1.4.dist-info}/WHEEL +0 -0
- {pydzn-0.1.3.dist-info → pydzn-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {pydzn-0.1.3.dist-info → pydzn-0.1.4.dist-info}/top_level.txt +0 -0
pydzn/components/__init__.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
from .button.component import Button
|
2
|
+
from .card.component import Card
|
2
3
|
from .text.component import Text
|
3
4
|
from .drawer.component import Drawer
|
4
5
|
from .sidebar.component import Sidebar
|
5
6
|
from .nav_item.component import NavItem
|
7
|
+
from .hamburger_menu.component import HamburgerMenu
|
6
8
|
|
7
9
|
|
8
|
-
__all__ = ["Button", "Text", "Drawer", "Sidebar", "NavItem"]
|
10
|
+
__all__ = ["Button", "Card", "Text", "Drawer", "Sidebar", "NavItem", "HamburgerMenu"]
|
@@ -1,26 +1,86 @@
|
|
1
1
|
from pydzn.base_component import BaseComponent
|
2
|
+
from pydzn.variants import VariantSupport
|
2
3
|
|
3
4
|
|
4
|
-
class Button(BaseComponent):
|
5
|
+
class Button(VariantSupport, BaseComponent):
|
5
6
|
"""
|
6
|
-
|
7
|
-
|
7
|
+
Server-rendered Button with pluggable variants/sizes.
|
8
|
+
|
9
|
+
- Built-in variants keep colors inline so it works without a theme pack.
|
10
|
+
- You can add external libraries and select with namespace, e.g. variant="acme:glass".
|
8
11
|
"""
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
13
|
+
# visual “structures” (kept color-aware so no theme pack needed)
|
14
|
+
VARIANTS = {
|
15
|
+
# solid
|
16
|
+
"solid-primary": (
|
17
|
+
"rounded-sm border border-transparent "
|
18
|
+
"bg-[#2563eb] text-[white] "
|
19
|
+
"shadow-md hover:shadow-lg"
|
20
|
+
),
|
21
|
+
# outline
|
22
|
+
"outline-primary": (
|
23
|
+
"rounded-sm border-2 border-blue-500 "
|
24
|
+
"bg-[transparent] text-[#2563eb] "
|
25
|
+
"shadow-sm hover:shadow-md"
|
26
|
+
),
|
27
|
+
# ghost (neutral)
|
28
|
+
"ghost-neutral": (
|
29
|
+
"rounded-sm border-0 "
|
30
|
+
"bg-[transparent] text-body "
|
31
|
+
"shadow-none hover:bg-[rgba(15,23,42,.06)]"
|
32
|
+
),
|
33
|
+
# linky button
|
34
|
+
"link-primary": (
|
35
|
+
"rounded-none border-0 "
|
36
|
+
"bg-[transparent] text-[#2563eb] "
|
37
|
+
"shadow-none hover:underline"
|
38
|
+
),
|
39
|
+
}
|
40
|
+
|
41
|
+
# density
|
42
|
+
SIZES = {
|
43
|
+
"sm": "px-3 py-1",
|
44
|
+
"md": "px-5 py-2",
|
45
|
+
"lg": "px-6 py-3",
|
46
|
+
"xl": "px-8 py-4",
|
47
|
+
}
|
48
|
+
|
49
|
+
# tones are optional here; variants carry color already
|
50
|
+
TONES = {}
|
51
|
+
|
52
|
+
# project-wide defaults can be overridden via VariantSupport.set_default_choices(Button, ...)
|
53
|
+
DEFAULTS = {
|
54
|
+
"variant": "outline-primary",
|
55
|
+
"size": "md",
|
56
|
+
"tone": "",
|
57
|
+
}
|
14
58
|
|
15
|
-
def __init__(
|
16
|
-
|
17
|
-
|
18
|
-
|
59
|
+
def __init__(
|
60
|
+
self,
|
61
|
+
text: str = "",
|
62
|
+
children: str | None = None,
|
63
|
+
*,
|
64
|
+
tag: str = "button",
|
65
|
+
# variant system
|
66
|
+
variant: str | None = None,
|
67
|
+
size: str | None = None,
|
68
|
+
tone: str | None = None,
|
69
|
+
# raw utility escape hatch (merged last)
|
70
|
+
dzn: str | None = None,
|
71
|
+
**attrs,
|
72
|
+
):
|
73
|
+
# allow stray attrs["dzn"] too
|
74
|
+
extra_dzn = dzn or attrs.pop("dzn", None)
|
19
75
|
|
20
|
-
#
|
21
|
-
effective_dzn =
|
76
|
+
# resolve VARIANT + SIZE (+ optional TONE) + extra_dzn
|
77
|
+
effective_dzn = self._resolve_variant_dzn(
|
78
|
+
variant=variant,
|
79
|
+
size=size,
|
80
|
+
tone=tone,
|
81
|
+
extra_dzn=extra_dzn,
|
82
|
+
)
|
22
83
|
|
23
|
-
# 4) Pass the merged dzn to BaseComponent (it will register + merge into class)
|
24
84
|
super().__init__(children=children, tag=tag, dzn=effective_dzn, **attrs)
|
25
85
|
self.text = text
|
26
86
|
|
@@ -1,15 +1,93 @@
|
|
1
1
|
from pydzn.base_component import BaseComponent
|
2
|
+
from pydzn.variants import VariantSupport
|
2
3
|
|
3
4
|
|
4
|
-
class Card(BaseComponent):
|
5
|
+
class Card(VariantSupport, BaseComponent):
|
5
6
|
"""
|
6
|
-
|
7
|
-
|
7
|
+
Server-rendered Card with pluggable variants/sizes/tones.
|
8
|
+
Variants focus on layout/structure; tones mainly adjust border color.
|
8
9
|
"""
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
# visual “structures”
|
12
|
+
VARIANTS = {
|
13
|
+
"panel": (
|
14
|
+
"flex flex-col gap-4 p-4 rounded-xl "
|
15
|
+
"border border-subtle bg-elevated shadow-sm"
|
16
|
+
),
|
17
|
+
"plain": (
|
18
|
+
"flex flex-col gap-3 p-4 rounded-md "
|
19
|
+
"border border-subtle bg-surface shadow-none"
|
20
|
+
),
|
21
|
+
"elevated": (
|
22
|
+
"flex flex-col gap-4 p-4 rounded-xl "
|
23
|
+
"border border-transparent bg-elevated shadow-lg"
|
24
|
+
),
|
25
|
+
"outlined": (
|
26
|
+
"flex flex-col gap-4 p-4 rounded-lg "
|
27
|
+
"border-2 border-slate-300 bg-[transparent] shadow-none"
|
28
|
+
),
|
29
|
+
"soft": (
|
30
|
+
"flex flex-col gap-4 p-4 rounded-xl "
|
31
|
+
"border-0 bg-[rgba(15,23,42,.03)] shadow-sm"
|
32
|
+
),
|
33
|
+
"glass": (
|
34
|
+
"flex flex-col gap-4 p-4 rounded-xl "
|
35
|
+
"border border-[rgba(255,255,255,.25)] bg-[rgba(255,255,255,.08)] shadow-md"
|
36
|
+
),
|
37
|
+
"ghost": (
|
38
|
+
"flex flex-col gap-4 p-4 rounded-md "
|
39
|
+
"border-0 bg-[transparent] shadow-none"
|
40
|
+
),
|
41
|
+
}
|
12
42
|
|
43
|
+
# density
|
44
|
+
SIZES = {
|
45
|
+
"xs": "p-2 gap-2 rounded-sm",
|
46
|
+
"sm": "p-3 gap-2 rounded-md",
|
47
|
+
"md": "p-4 gap-3 rounded-lg",
|
48
|
+
"lg": "p-6 gap-4 rounded-xl",
|
49
|
+
"xl": "p-8 gap-5 rounded-2xl",
|
50
|
+
}
|
51
|
+
|
52
|
+
# tones mainly drive border color (kept subtle by default)
|
53
|
+
TONES = {
|
54
|
+
"neutral": "border-subtle",
|
55
|
+
"primary": "border-blue-500",
|
56
|
+
"success": "border-green-500",
|
57
|
+
"danger": "border-red-500",
|
58
|
+
}
|
59
|
+
|
60
|
+
# project-wide defaults can be overridden at runtime:
|
61
|
+
# Card.set_default_choices(variant="...", size="...", tone="...")
|
62
|
+
DEFAULTS = {
|
63
|
+
"variant": "panel",
|
64
|
+
"size": "md",
|
65
|
+
"tone": "neutral",
|
66
|
+
}
|
67
|
+
|
68
|
+
def __init__(
|
69
|
+
self,
|
70
|
+
children: str | None = None,
|
71
|
+
*,
|
72
|
+
tag: str = "div",
|
73
|
+
# variant system
|
74
|
+
variant: str | None = None,
|
75
|
+
size: str | None = None,
|
76
|
+
tone: str | None = None,
|
77
|
+
# raw utility escape hatch (merged last)
|
78
|
+
dzn: str | None = None,
|
79
|
+
**attrs,
|
80
|
+
):
|
81
|
+
extra_dzn = dzn or attrs.pop("dzn", None)
|
82
|
+
|
83
|
+
effective_dzn = self._resolve_variant_dzn(
|
84
|
+
variant=variant,
|
85
|
+
size=size,
|
86
|
+
tone=tone,
|
87
|
+
extra_dzn=extra_dzn,
|
88
|
+
)
|
89
|
+
|
90
|
+
super().__init__(children=children, tag=tag, dzn=effective_dzn, **attrs)
|
13
91
|
|
14
92
|
def context(self) -> dict:
|
15
93
|
return {}
|
@@ -0,0 +1,210 @@
|
|
1
|
+
import uuid, re
|
2
|
+
from pydzn.base_component import BaseComponent
|
3
|
+
from pydzn.variants import VariantSupport
|
4
|
+
from pydzn.dzn import register_dzn_classes
|
5
|
+
|
6
|
+
|
7
|
+
def _hamburger_svg(sz=24):
|
8
|
+
return f'''
|
9
|
+
<svg width="{sz}" height="{sz}" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
10
|
+
<rect x="3" y="6" width="18" height="2" rx="1" fill="currentColor"/>
|
11
|
+
<rect x="3" y="11" width="18" height="2" rx="1" fill="currentColor"/>
|
12
|
+
<rect x="3" y="16" width="18" height="2" rx="1" fill="currentColor"/>
|
13
|
+
</svg>
|
14
|
+
'''.strip()
|
15
|
+
|
16
|
+
def _close_svg(sz=24):
|
17
|
+
return f'''
|
18
|
+
<svg width="{sz}" height="{sz}" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
19
|
+
<path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
20
|
+
</svg>
|
21
|
+
'''.strip()
|
22
|
+
|
23
|
+
|
24
|
+
class HamburgerMenu(VariantSupport, BaseComponent):
|
25
|
+
VARIANTS = {
|
26
|
+
# Trigger styles
|
27
|
+
"trigger:quiet": (
|
28
|
+
"px-3 py-2 rounded-md border-0 "
|
29
|
+
"bg-[transparent] text-[#333] hover:text-[#6b6969] cursor-pointer"
|
30
|
+
),
|
31
|
+
"trigger:solid": (
|
32
|
+
"px-3 py-2 rounded-md border border-[1px] "
|
33
|
+
"bg-[#2563eb] text-[white] hover:bg-[rgba(37,99,235,.92)] cursor-pointer"
|
34
|
+
),
|
35
|
+
|
36
|
+
# Panel styles: no default background here
|
37
|
+
"panel:sheet": "text-[black] shadow-xl rounded-[10px]",
|
38
|
+
"panel:glass": "text-[black] shadow-lg rounded-[10px] backdrop-blur bg-[rgba(255,255,255,.70)]",
|
39
|
+
"panel:bare": "text-[inherit] shadow-none",
|
40
|
+
}
|
41
|
+
|
42
|
+
def __init__(self, *,
|
43
|
+
children: str = "",
|
44
|
+
mode: str = "right",
|
45
|
+
drawer_width: int | str = 320,
|
46
|
+
dropdown_height: int | str = 320,
|
47
|
+
show_backdrop: bool = True,
|
48
|
+
icon_size: int = 24,
|
49
|
+
trigger_variant: str | None = None,
|
50
|
+
trigger_size: str | None = None,
|
51
|
+
panel_variant: str | None = None,
|
52
|
+
dzn: str | None = None,
|
53
|
+
button_dzn: str | None = None,
|
54
|
+
panel_dzn: str | None = None,
|
55
|
+
**attrs,
|
56
|
+
):
|
57
|
+
self._uid = uuid.uuid4().hex[:8]
|
58
|
+
self.mode = mode
|
59
|
+
self.drawer_width = int(drawer_width)
|
60
|
+
self.dropdown_height = int(dropdown_height)
|
61
|
+
self.show_backdrop = bool(show_backdrop)
|
62
|
+
self.icon_size = int(icon_size)
|
63
|
+
|
64
|
+
# Resolve trigger/panel via VariantSupport
|
65
|
+
self._trigger_dzn = self._resolve_variant_dzn(
|
66
|
+
variant=trigger_variant or "trigger:quiet",
|
67
|
+
size=trigger_size or "md",
|
68
|
+
tone=None,
|
69
|
+
extra_dzn=(button_dzn or "")
|
70
|
+
)
|
71
|
+
self._panel_dzn = self._resolve_variant_dzn(
|
72
|
+
variant=panel_variant or "panel:sheet",
|
73
|
+
size=None,
|
74
|
+
tone=None,
|
75
|
+
extra_dzn=(panel_dzn or "")
|
76
|
+
)
|
77
|
+
|
78
|
+
# Wrapper classes + auto-forward visual tokens to the panel
|
79
|
+
wrapper_core = "relative inline-block"
|
80
|
+
base_wrapper = f"{wrapper_core} {dzn.strip()}" if dzn else wrapper_core
|
81
|
+
forwarded = self._extract_visual_tokens(dzn or "")
|
82
|
+
self._panel_forwarded = forwarded
|
83
|
+
|
84
|
+
# Register for /_dzn.css emission
|
85
|
+
register_dzn_classes(base_wrapper)
|
86
|
+
register_dzn_classes(self._trigger_dzn)
|
87
|
+
register_dzn_classes(self._panel_dzn)
|
88
|
+
register_dzn_classes(self._panel_forwarded)
|
89
|
+
register_dzn_classes(
|
90
|
+
"flex items-center gap-2 hidden fixed inset-0 absolute top-0 bottom-0 "
|
91
|
+
"right-0 left-0 block overflow-auto border rounded-[10px] "
|
92
|
+
"z-[10000] z-[10001]"
|
93
|
+
)
|
94
|
+
|
95
|
+
super().__init__(children=children, tag="div", dzn=base_wrapper, **attrs)
|
96
|
+
|
97
|
+
def context(self) -> dict:
|
98
|
+
return {}
|
99
|
+
|
100
|
+
# --- helpers ---
|
101
|
+
@staticmethod
|
102
|
+
def _extract_visual_tokens(s: str) -> str:
|
103
|
+
if not s: return ""
|
104
|
+
keep = []
|
105
|
+
for cls in s.split():
|
106
|
+
if (cls.startswith("bg-[") or cls.startswith("text-[")
|
107
|
+
or cls.startswith("border") or cls.startswith("rounded")
|
108
|
+
or cls.startswith("shadow")):
|
109
|
+
keep.append(cls)
|
110
|
+
return " ".join(keep)
|
111
|
+
|
112
|
+
@staticmethod
|
113
|
+
def _has_bg_token(s: str) -> bool:
|
114
|
+
return bool(re.search(r"\bbg-\[", s or ""))
|
115
|
+
|
116
|
+
# --- render ---
|
117
|
+
def render(self) -> str:
|
118
|
+
uid = self._uid
|
119
|
+
root_cls = f"hm-{uid}"
|
120
|
+
cb_id = f"hmcb-{uid}"
|
121
|
+
drawer_id = f"hm-drawer-{uid}"
|
122
|
+
|
123
|
+
css = f"""
|
124
|
+
<style>
|
125
|
+
.{root_cls} .hm__icon--close {{ display:none; }}
|
126
|
+
.{root_cls} input[type="checkbox"] {{ position:absolute; opacity:0; pointer-events:none; }}
|
127
|
+
.{root_cls} .hm__toggle {{ display:inline-flex; align-items:center; gap:.5rem; }}
|
128
|
+
|
129
|
+
.{root_cls} .hm__drawer {{
|
130
|
+
transition: transform .22s ease, opacity .22s ease;
|
131
|
+
will-change: transform, opacity;
|
132
|
+
outline: none;
|
133
|
+
}}
|
134
|
+
.{root_cls}[data-mode="right"] .hm__drawer {{ transform: translateX(100%); }}
|
135
|
+
.{root_cls}[data-mode="left"] .hm__drawer {{ transform: translateX(-100%); }}
|
136
|
+
.{root_cls}[data-mode="dropdown"] .hm__drawer {{ transform: translateY(-8px); opacity:0; }}
|
137
|
+
|
138
|
+
.{root_cls} input:checked ~ label .hm__icon--hamburger {{ display:none; }}
|
139
|
+
.{root_cls} input:checked ~ label .hm__icon--close {{ display:inline-block; }}
|
140
|
+
|
141
|
+
.{root_cls}[data-mode="right"] input:checked ~ .hm__backdrop {{ display:block; }}
|
142
|
+
.{root_cls}[data-mode="left"] input:checked ~ .hm__backdrop {{ display:block; }}
|
143
|
+
.{root_cls}[data-mode="dropdown"] .hm__backdrop {{ display:none !important; }}
|
144
|
+
|
145
|
+
.{root_cls}[data-mode="right"] input:checked ~ .hm__drawer {{ transform: translateX(0); }}
|
146
|
+
.{root_cls}[data-mode="left"] input:checked ~ .hm__drawer {{ transform: translateX(0); }}
|
147
|
+
.{root_cls}[data-mode="dropdown"] input:checked ~ .hm__drawer {{ transform: translateY(0); opacity:1; }}
|
148
|
+
</style>
|
149
|
+
""".strip()
|
150
|
+
|
151
|
+
backdrop_html = ""
|
152
|
+
if self.show_backdrop and self.mode in ("left", "right"):
|
153
|
+
backdrop_html = (
|
154
|
+
f'<label for="{cb_id}" class="hm__backdrop hidden fixed inset-0 bg-[rgba(0,0,0,.35)] z-[10000]" aria-hidden="true"></label>'
|
155
|
+
)
|
156
|
+
|
157
|
+
trigger_html = f"""
|
158
|
+
<label for="{cb_id}" class="hm__toggle {self._trigger_dzn} flex items-center gap-2" aria-controls="{drawer_id}" aria-haspopup="true" style="cursor:pointer">
|
159
|
+
<span class="hm__icon hm__icon--hamburger" aria-hidden="true">{_hamburger_svg(self.icon_size)}</span>
|
160
|
+
<span class="hm__icon hm__icon--close" aria-hidden="true">{_close_svg(self.icon_size)}</span>
|
161
|
+
</label>
|
162
|
+
""".strip()
|
163
|
+
|
164
|
+
# Drawer base position/size
|
165
|
+
if self.mode in ("left", "right"):
|
166
|
+
side = "right-0" if self.mode == "right" else "left-0"
|
167
|
+
drawer_base = f"fixed top-0 bottom-0 {side} overflow-auto z-[10001] w-[{self.drawer_width}px]"
|
168
|
+
else:
|
169
|
+
drawer_base = (
|
170
|
+
f"absolute top-[calc(100%+8px)] right-0 "
|
171
|
+
f"h-[{self.dropdown_height}px] w-[min(92vw,420px)] "
|
172
|
+
f"overflow-auto border rounded-[10px] z-[10001]"
|
173
|
+
)
|
174
|
+
register_dzn_classes(drawer_base)
|
175
|
+
|
176
|
+
# Panel classes = variant + forwarded visual tokens (+ default bg if none provided)
|
177
|
+
panel_classes = f"{self._panel_dzn} {self._panel_forwarded}".strip()
|
178
|
+
if not self._has_bg_token(panel_classes):
|
179
|
+
panel_classes = f"{panel_classes} bg-[white]"
|
180
|
+
register_dzn_classes(panel_classes)
|
181
|
+
|
182
|
+
drawer_html = f"""
|
183
|
+
<div id="{drawer_id}" class="hm__drawer {drawer_base} {panel_classes}">
|
184
|
+
{self.children or ""}
|
185
|
+
</div>
|
186
|
+
""".strip()
|
187
|
+
|
188
|
+
checkbox = f'<input id="{cb_id}" type="checkbox" />'
|
189
|
+
|
190
|
+
return (
|
191
|
+
css
|
192
|
+
+ f'<div class="{root_cls}" data-mode="{self.mode}" {self._attrs_string()}>'
|
193
|
+
+ checkbox
|
194
|
+
+ trigger_html
|
195
|
+
+ backdrop_html
|
196
|
+
+ drawer_html
|
197
|
+
+ "</div>"
|
198
|
+
)
|
199
|
+
|
200
|
+
def _attrs_string(self) -> str:
|
201
|
+
import html as _html
|
202
|
+
attr_parts = []
|
203
|
+
for k, v in self.html_attrs.items():
|
204
|
+
if k == "style":
|
205
|
+
continue
|
206
|
+
attr_parts.append(f'{k}="{_html.escape(str(v), quote=True)}"')
|
207
|
+
style_str = self._style_string()
|
208
|
+
if style_str:
|
209
|
+
attr_parts.append(f'style="{_html.escape(style_str, quote=True)}"')
|
210
|
+
return " ".join(attr_parts)
|
@@ -1,56 +1,220 @@
|
|
1
1
|
from pydzn.base_component import BaseComponent
|
2
|
+
from pydzn.variants import VariantSupport
|
2
3
|
from pydzn.dzn import register_dzn_classes
|
3
4
|
|
4
5
|
|
5
|
-
class NavItem(BaseComponent):
|
6
|
+
class NavItem(VariantSupport, BaseComponent):
|
6
7
|
"""
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
Nav item with pluggable variants + fluent helpers so users don't need DZN utilities.
|
9
|
+
|
10
|
+
Example:
|
11
|
+
NavItem(variant="sidebar-underline", children=Text("Overview").render())
|
12
|
+
.center()
|
13
|
+
.height(64)
|
14
|
+
.full_width()
|
15
|
+
.bottom_divider("subtle")
|
16
|
+
.hover(bg="rgba(37,99,235,.06)", text="#2563eb", underline="blue-500")
|
17
|
+
.focus(bg="rgba(37,99,235,.10)")
|
18
|
+
.render()
|
12
19
|
"""
|
13
20
|
|
21
|
+
# -------- Variants --------
|
22
|
+
VARIANTS = {
|
23
|
+
# Sidebar family
|
24
|
+
"sidebar-default": (
|
25
|
+
"flex items-center justify-center text-center "
|
26
|
+
"rounded-none border-solid border-b border-subtle "
|
27
|
+
"text-body hover:bg-[rgba(15,23,42,.06)]"
|
28
|
+
),
|
29
|
+
"sidebar-compact": (
|
30
|
+
"flex items-center justify-center text-center "
|
31
|
+
"rounded-none border-solid border-b border-subtle "
|
32
|
+
"text-body hover:bg-[rgba(15,23,42,.06)]"
|
33
|
+
),
|
34
|
+
"sidebar-active": (
|
35
|
+
"flex items-center justify-center text-center "
|
36
|
+
"rounded-none border-solid border-b border-subtle "
|
37
|
+
"bg-[rgba(37,99,235,.10)] text-[#2563eb]"
|
38
|
+
),
|
39
|
+
"sidebar-quiet": (
|
40
|
+
"flex items-center justify-center text-center "
|
41
|
+
"rounded-none border-solid border-b border-subtle "
|
42
|
+
"text-[rgb(100,116,139)] hover:bg-[rgba(15,23,42,.04)]"
|
43
|
+
),
|
44
|
+
# underline-only (no box borders)
|
45
|
+
"sidebar-underline": (
|
46
|
+
"flex items-center justify-center text-center "
|
47
|
+
"rounded-none border-0 border-b border-subtle border-solid py-2"
|
48
|
+
),
|
49
|
+
# square tile w/ underline + hovers
|
50
|
+
"sidebar-squared-underline": (
|
51
|
+
"flex items-center justify-center text-center "
|
52
|
+
"rounded-none border-0 border-b border-subtle border-solid py-2 "
|
53
|
+
"hover:bg-[rgba(37,99,235,.06)] hover:border-blue-500 hover:text-[#2563eb] "
|
54
|
+
"focus:bg-[rgba(37,99,235,.10)]"
|
55
|
+
),
|
56
|
+
|
57
|
+
# Dropdown family
|
58
|
+
"dropdown-item": (
|
59
|
+
"px-3 py-2 rounded-md border-0 "
|
60
|
+
"bg-[transparent] text-body hover:bg-[rgba(15,23,42,.06)]"
|
61
|
+
),
|
62
|
+
"dropdown-accent": (
|
63
|
+
"w-[100%] px-3 py-2 rounded-md border-0 "
|
64
|
+
"bg-[transparent] text-[#2563eb] hover:bg-[rgba(37,99,235,.08)]"
|
65
|
+
),
|
66
|
+
"dropdown-danger": (
|
67
|
+
"w-[100%] px-3 py-2 rounded-md border-0 "
|
68
|
+
"bg-[transparent] text-[rgb(239,68,68)] hover:bg-[rgba(239,68,68,.08)]"
|
69
|
+
),
|
70
|
+
"dropdown-plain": (
|
71
|
+
"w-[100%] px-3 py-2 rounded-none border-0 "
|
72
|
+
"bg-[transparent] text-body hover:bg-[rgba(15,23,42,.04)]"
|
73
|
+
),
|
74
|
+
|
75
|
+
# Simple family
|
76
|
+
"simple-item": (
|
77
|
+
"px-3 py-2 rounded-none border-0 bg-[transparent] no-underline "
|
78
|
+
"text-[#333] hover:text-[#6b6969]"
|
79
|
+
),
|
80
|
+
|
81
|
+
}
|
82
|
+
|
83
|
+
SIZES = {
|
84
|
+
"sm": "text-[12px]",
|
85
|
+
"md": "text-[14px]",
|
86
|
+
"lg": "text-[16px]",
|
87
|
+
}
|
88
|
+
|
89
|
+
TONES = {
|
90
|
+
"muted": "text-[rgb(100,116,139)]",
|
91
|
+
"primary": "text-[#2563eb]",
|
92
|
+
"danger": "text-[rgb(239,68,68)]",
|
93
|
+
}
|
94
|
+
|
95
|
+
DEFAULTS = {"variant": "sidebar-default", "size": "md", "tone": ""}
|
96
|
+
|
97
|
+
# -------- ctor --------
|
14
98
|
def __init__(
|
15
99
|
self,
|
16
100
|
*,
|
17
101
|
children: str | None = None,
|
18
102
|
tag: str = "div",
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
103
|
+
variant: str | None = None,
|
104
|
+
size: str | None = None,
|
105
|
+
tone: str | None = None,
|
106
|
+
dzn: str | None = None, # extra raw utilities merged last
|
23
107
|
**attrs,
|
24
108
|
):
|
25
|
-
|
26
|
-
self._active_classes = list(active_classes or [])
|
27
|
-
|
28
|
-
# If we'll toggle classes at runtime, pre-register them so /_dzn.css emits rules.
|
29
|
-
if self._active_classes:
|
30
|
-
register_dzn_classes(self._active_classes)
|
109
|
+
extra_dzn = dzn or attrs.pop("dzn", None)
|
31
110
|
|
32
|
-
#
|
33
|
-
effective_dzn = (dzn or "")
|
34
|
-
if active and self._active_classes:
|
35
|
-
effective_dzn = (effective_dzn + " " + " ".join(self._active_classes)).strip()
|
36
|
-
|
37
|
-
# Sensible a11y defaults (still “unstyled”)
|
111
|
+
# minimal a11y defaults
|
38
112
|
attrs.setdefault("role", "button")
|
39
113
|
attrs.setdefault("tabindex", "0")
|
40
114
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
115
|
+
base = self._resolve_variant_dzn(
|
116
|
+
variant=variant,
|
117
|
+
size=size,
|
118
|
+
tone=tone,
|
119
|
+
extra_dzn=extra_dzn,
|
120
|
+
)
|
121
|
+
|
122
|
+
# keep base vs. runtime styles separate
|
123
|
+
self._base_dzn = base.strip()
|
124
|
+
self._runtime_dzn: list[str] = []
|
125
|
+
|
126
|
+
# pre-register everything we know at init
|
127
|
+
register_dzn_classes(self._base_dzn)
|
128
|
+
|
129
|
+
# let BaseComponent merge base dzn into class
|
130
|
+
super().__init__(children=children or "", tag=tag, dzn=self._base_dzn, **attrs)
|
131
|
+
|
132
|
+
# -------- internal helpers --------
|
133
|
+
def _merge_all(self) -> str:
|
134
|
+
return " ".join([self._base_dzn] + self._runtime_dzn).strip()
|
135
|
+
|
136
|
+
def _add_runtime(self, *classes: str) -> None:
|
137
|
+
cls_str = " ".join(c for c in classes if c)
|
138
|
+
if not cls_str:
|
139
|
+
return
|
140
|
+
# register for CSS emission
|
141
|
+
register_dzn_classes(cls_str)
|
142
|
+
# track runtime classes
|
143
|
+
self._runtime_dzn.extend(c for c in cls_str.split() if c)
|
144
|
+
# immediately apply to html_attrs["class"] so render sees it
|
145
|
+
merged = self._merge_all()
|
146
|
+
self.html_attrs["class"] = self._merge_classes(self.html_attrs.get("class", ""), merged)
|
147
|
+
|
148
|
+
# -------- fluent builder methods --------
|
149
|
+
def center(self, *, x: bool = True, y: bool = True, text: bool = True):
|
150
|
+
if x or y:
|
151
|
+
self._add_runtime("flex")
|
152
|
+
if x:
|
153
|
+
self._add_runtime("justify-center")
|
154
|
+
if y:
|
155
|
+
self._add_runtime("items-center")
|
156
|
+
if text:
|
157
|
+
self._add_runtime("text-center")
|
158
|
+
return self
|
159
|
+
|
160
|
+
def height(self, px: int):
|
161
|
+
self._add_runtime(f"h-[{px}px]")
|
162
|
+
return self
|
163
|
+
|
164
|
+
def full_width(self, on: bool = True):
|
165
|
+
if on:
|
166
|
+
self._add_runtime("w-[100%]")
|
167
|
+
return self
|
168
|
+
|
169
|
+
def bottom_divider(self, color: str = "subtle", *, style: str = "solid"):
|
170
|
+
# ensure only the bottom edge shows
|
171
|
+
self._add_runtime("border-0", "border-b", f"border-{color}", f"border-{style}")
|
172
|
+
return self
|
173
|
+
|
174
|
+
def hover(self, *, bg: str | None = None, text: str | None = None, underline: str | None = None):
|
175
|
+
if bg:
|
176
|
+
self._add_runtime(f"hover:bg-[{bg}]")
|
177
|
+
if text:
|
178
|
+
self._add_runtime(f"hover:text-[{text}]")
|
179
|
+
if underline:
|
180
|
+
self._add_runtime(f"hover:border-{underline}")
|
181
|
+
return self
|
182
|
+
|
183
|
+
def focus(self, *, bg: str | None = None):
|
184
|
+
if bg:
|
185
|
+
self._add_runtime(f"focus:bg-[{bg}]")
|
186
|
+
return self
|
187
|
+
|
188
|
+
def padding(self, *, all: int | str | None = None, x: int | str | None = None, y: int | str | None = None):
|
189
|
+
def _choose(prefix: str, val):
|
190
|
+
if val is None:
|
191
|
+
return
|
192
|
+
if isinstance(val, int):
|
193
|
+
self._add_runtime(f"{prefix}-{val}")
|
194
|
+
else:
|
195
|
+
self._add_runtime(f"{prefix}-[{val}]")
|
196
|
+
_choose("p", all)
|
197
|
+
_choose("px", x)
|
198
|
+
_choose("py", y)
|
199
|
+
return self
|
200
|
+
|
201
|
+
def reset_runtime_styles(self):
|
202
|
+
"""Remove everything added by builder methods (keeps the chosen variant)."""
|
203
|
+
self._runtime_dzn = []
|
204
|
+
# reset class to just base
|
205
|
+
self.html_attrs["class"] = self._base_dzn
|
206
|
+
register_dzn_classes(self._base_dzn)
|
207
|
+
return self
|
51
208
|
|
52
|
-
|
209
|
+
def as_link(self, href: str, *, new_tab: bool=False):
|
210
|
+
self.tag = "a"
|
211
|
+
self.html_attrs["href"] = href
|
212
|
+
self.html_attrs["role"] = "link"
|
213
|
+
if new_tab:
|
214
|
+
self.html_attrs["target"] = "_blank"
|
215
|
+
self.html_attrs["rel"] = "noopener noreferrer"
|
216
|
+
return self
|
53
217
|
|
218
|
+
# required by BaseComponent
|
54
219
|
def context(self) -> dict:
|
55
|
-
# No label here; use children (e.g., a Text component) for content.
|
56
220
|
return {}
|