Skip to content

Commit ff559a1

Browse files
committed
update components
1 parent 9445aee commit ff559a1

File tree

6 files changed

+392
-90
lines changed

6 files changed

+392
-90
lines changed

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
- **Dev-only**: add this library as a development dependency (no runtime import)
1616

1717
- **With uv (recommended)**:
18-
- Direct from git: `uv add --dev git+https://github.com/dakixr/htpy-uikit.git`
19-
- Local checkout: `uv add --dev ../path/to/htpy-uikit`
20-
- Run CLI via your env: `uv run htpyuikit list`
21-
- **Or with pip**: `pip install .` (inside this repo)
18+
- `uv tool install git+https://github.com/dakixr/htpy-uikit.git`
19+
- **Or with pip**:
20+
- `pip install git+https://github.com/dakixr/htpy-uikit.git`
2221

2322
## CLI
2423

src/htpy_uikit/components/__init__.py

Whitespace-only changes.

src/htpy_uikit/components/icons.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,42 @@ def icon_check(class_: str = "size-4", **attrs) -> Renderable:
200200
)[path(d="M20 6L9 17l-5-5"),]
201201

202202

203+
def icon_pencil(class_: str = "size-4", **attrs) -> Renderable:
204+
"""Pencil/edit icon."""
205+
return svg(
206+
class_=class_,
207+
xmlns="http://www.w3.org/2000/svg",
208+
viewBox="0 0 24 24",
209+
fill="none",
210+
stroke="currentColor",
211+
stroke_width="2",
212+
stroke_linecap="round",
213+
stroke_linejoin="round",
214+
**attrs,
215+
)[
216+
path(
217+
d="m14.304 4.844 2.852 2.852M7 7H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-4.5m2.409-9.91a2.017 2.017 0 0 1 0 2.853l-6.844 6.844L8 14l.713-3.565 6.844-6.844a2.015 2.015 0 0 1 2.852 0Z",
218+
)
219+
]
220+
221+
222+
def icon_headset(class_: str = "size-4", **attrs) -> Renderable:
223+
"""Headset/support icon."""
224+
return svg(
225+
class_=class_,
226+
xmlns="http://www.w3.org/2000/svg",
227+
viewBox="0 0 24 24",
228+
fill="currentColor",
229+
**attrs,
230+
)[
231+
path(
232+
fill_rule="evenodd",
233+
d="M12 2a7 7 0 0 0-7 7 3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1a1 1 0 0 0 1-1V9a5 5 0 1 1 10 0v7.083A2.919 2.919 0 0 1 14.083 19H14a2 2 0 0 0-2-2h-1a2 2 0 0 0-2 2v1a2 2 0 0 0 2 2h1a2 2 0 0 0 1.732-1h.351a4.917 4.917 0 0 0 4.83-4H19a3 3 0 0 0 3-3v-2a3 3 0 0 0-3-3 7 7 0 0 0-7-7Zm1.45 3.275a4 4 0 0 0-4.352.976 1 1 0 0 0 1.452 1.376 2.001 2.001 0 0 1 2.836-.067 1 1 0 1 0 1.386-1.442 4 4 0 0 0-1.321-.843Z",
234+
clip_rule="evenodd",
235+
)
236+
]
237+
238+
203239
def icon_close(class_: str = "size-4", **attrs) -> Renderable:
204240
"""Close/X icon."""
205241
return svg(
@@ -598,3 +634,118 @@ def icon_eye_off(class_: str = "w-5 h-5", **attrs) -> Renderable:
598634
d="M3.933 13.909A4.357 4.357 0 0 1 3 12c0-1 4-6 9-6m7.6 3.8A5.068 5.068 0 0 1 21 12c0 1-3 6-9 6-.314 0-.62-.014-.918-.04M5 19 19 5m-4 7a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z",
599635
)
600636
]
637+
638+
639+
def icon_tag(class_: str = "size-4", **attrs) -> Renderable:
640+
"""
641+
Tag/flag icon for active data sources.
642+
643+
Args:
644+
class_: CSS classes for the SVG element.
645+
**attrs: Additional SVG attributes.
646+
647+
Returns:
648+
Renderable: SVG tag icon.
649+
"""
650+
return svg(
651+
class_=class_,
652+
aria_hidden="true",
653+
xmlns="http://www.w3.org/2000/svg",
654+
width="24",
655+
height="24",
656+
fill="currentColor",
657+
viewBox="0 0 24 24",
658+
**attrs,
659+
)[
660+
path(
661+
fill_rule="evenodd",
662+
d="M3 4a1 1 0 0 0-.822 1.57L6.632 12l-4.454 6.43A1 1 0 0 0 3 20h13.153a1 1 0 0 0 .822-.43l4.847-7a1 1 0 0 0 0-1.14l-4.847-7a1 1 0 0 0-.822-.43H3Z",
663+
clip_rule="evenodd",
664+
)
665+
]
666+
667+
668+
def icon_plus(class_: str = "size-4", **attrs) -> Renderable:
669+
"""
670+
Plus/add icon.
671+
672+
Args:
673+
class_: CSS classes for the SVG element.
674+
**attrs: Additional SVG attributes.
675+
676+
Returns:
677+
Renderable: SVG plus icon.
678+
"""
679+
return svg(
680+
class_=class_,
681+
xmlns="http://www.w3.org/2000/svg",
682+
fill="none",
683+
viewBox="0 0 24 24",
684+
stroke_width="1.5",
685+
stroke="currentColor",
686+
**attrs,
687+
)[
688+
path(
689+
stroke_linecap="round",
690+
stroke_linejoin="round",
691+
d="M12 4.5v15m7.5-7.5h-15",
692+
)
693+
]
694+
695+
696+
def icon_sort_asc(class_: str = "w-3 h-3", **attrs) -> Renderable:
697+
"""
698+
Sort ascending arrow icon.
699+
700+
Args:
701+
class_: CSS classes for the SVG element.
702+
**attrs: Additional SVG attributes.
703+
704+
Returns:
705+
Renderable: SVG sort ascending arrow icon.
706+
"""
707+
return svg(
708+
class_=class_,
709+
aria_hidden="true",
710+
xmlns="http://www.w3.org/2000/svg",
711+
fill="none",
712+
viewBox="0 0 10 14",
713+
**attrs,
714+
)[
715+
path(
716+
stroke="currentColor",
717+
stroke_linecap="round",
718+
stroke_linejoin="round",
719+
stroke_width="2",
720+
d="M5 13V1m0 0L1 5m4-4 4 4",
721+
)
722+
]
723+
724+
725+
def icon_sort_desc(class_: str = "w-3 h-3", **attrs) -> Renderable:
726+
"""
727+
Sort descending arrow icon.
728+
729+
Args:
730+
class_: CSS classes for the SVG element.
731+
**attrs: Additional SVG attributes.
732+
733+
Returns:
734+
Renderable: SVG sort descending arrow icon.
735+
"""
736+
return svg(
737+
class_=class_,
738+
aria_hidden="true",
739+
xmlns="http://www.w3.org/2000/svg",
740+
fill="none",
741+
viewBox="0 0 10 14",
742+
**attrs,
743+
)[
744+
path(
745+
stroke="currentColor",
746+
stroke_linecap="round",
747+
stroke_linejoin="round",
748+
stroke_width="2",
749+
d="M5 1v12m0 0 4-4m-4 4L1 9",
750+
)
751+
]

src/htpy_uikit/components/modal.py

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from htpy import span
77
from htpy import with_children
88
from markupsafe import Markup
9+
from sourcetypes import js
910

1011
from ._utils import random_string
1112
from .icons import icon_close
@@ -18,7 +19,6 @@ def _modal_panel(
1819
width: str,
1920
height: str,
2021
close_button_attrs: dict[str, str] | None = None,
21-
panel_attrs: dict[str, str] | None = None,
2222
) -> Renderable:
2323
"""
2424
Shared modal panel content using consistent styles.
@@ -36,9 +36,8 @@ def _modal_panel(
3636

3737
panel_kwargs: dict[str, str] = {
3838
"class_": f"bg-card text-card-foreground rounded-lg shadow-lg border border-border flex flex-col {width} {height}",
39+
"@click.stop": "",
3940
}
40-
if panel_attrs:
41-
panel_kwargs |= panel_attrs
4241

4342
button_kwargs: dict[str, str] = {
4443
"type": "button",
@@ -84,8 +83,22 @@ def modal(
8483
htpy.div: Modal element
8584
"""
8685

86+
at_modal_open: js = f"""
87+
if ($event.detail === '{id}') {{
88+
console.log('modal-open: ', '{id}');
89+
show = true;
90+
}}
91+
"""
92+
93+
at_modal_close: js = f"""
94+
if ($event.detail === '{id}') {{
95+
console.log('modal-close: ', '{id}');
96+
show = false;
97+
}}
98+
"""
99+
87100
return div(
88-
id=id,
101+
# id=id,
89102
tabindex="-1",
90103
x_cloak="true",
91104
x_data="{ show: false }",
@@ -95,18 +108,17 @@ def modal(
95108
"inset-0 h-full max-h-full bg-black/50"
96109
),
97110
**{
98-
"@modal-open.window": Markup(f"if ($event.detail === '{id}') show = true;"),
99-
"@modal-close.window": Markup(f"if ($event.detail === '{id}') show = false;"),
100-
"@click": "show = false",
111+
"@modal-open.window": Markup(at_modal_open),
112+
"@modal-close.window": Markup(at_modal_close),
101113
},
114+
**attrs_btn_close_modal(id),
102115
)[
103116
_modal_panel(
104117
children,
105118
title=title,
106119
width=width,
107120
height=height,
108-
close_button_attrs={"@click": "show = false"},
109-
panel_attrs={"@click.stop": ""},
121+
close_button_attrs=attrs_btn_close_modal(id),
110122
)
111123
]
112124

@@ -129,27 +141,49 @@ def attrs_btn_open_modal(id: str) -> dict:
129141
}
130142

131143

144+
def attrs_btn_close_modal(id: str) -> dict:
145+
"""
146+
Get attributes for closing a modal button using Alpine.js events.
147+
148+
Note: this can only be used inside a modal component (or any other component that uses x-data).
149+
"""
150+
close_modal: js = f"""
151+
window.dispatchEvent(new CustomEvent('modal-close', {{ detail: '{id}' }}));
152+
"""
153+
return {
154+
# we do not use the x-data because this is a button is inside the model which is already using x-data
155+
"@click": Markup(close_modal),
156+
"@keydown.escape.window": Markup(close_modal),
157+
}
158+
159+
132160
@with_children
133161
def hx_modal(
134162
children: Node,
135163
*,
164+
id: str | None = None,
136165
title: str,
137166
width: str = "w-full max-w-lg",
138167
height: str = "h-auto",
139168
) -> Renderable:
140169
"""
141-
Alpine.js modal component (formerly HTMX).
170+
Alpine.js modal component (designed to be used with HTMX).
142171
143172
Args:
173+
id: Modal ID (if not provided, a random ID will be generated)
144174
title: Modal title
145-
content: Modal content
146175
width: Modal width classes
147176
height: Modal height classes
148177
149178
Returns:
150179
htpy.div: Alpine.js modal element
151180
"""
152-
random_id = random_string(8)
181+
random_id = id or random_string(8)
182+
183+
delete_modal: js = """
184+
show = false;
185+
setTimeout(() => $el.remove(), 200)
186+
"""
153187

154188
return div(
155189
tabindex="-1",
@@ -161,19 +195,16 @@ def hx_modal(
161195
"fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-screen "
162196
"inset-0 h-full max-h-full bg-black/50"
163197
),
164-
**{"@click": "show = false; setTimeout(() => $el.remove(), 200)"},
198+
**{"@click": delete_modal},
165199
)[
166200
_modal_panel(
167201
children,
168202
title=title,
169203
width=width,
170204
height=height,
171205
close_button_attrs={
172-
"@click": "show = false; setTimeout(() => $el.remove(), 200)",
173-
"type": "button",
174-
},
175-
panel_attrs={
176-
"@click.stop": "",
206+
"@click": Markup(delete_modal),
207+
"@keydown.escape.window": Markup(delete_modal),
177208
},
178209
)
179210
]

0 commit comments

Comments
 (0)