htmui 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.
- htmui/__init__.py +1 -0
- htmui/basecoat/__init__.py +0 -0
- htmui/basecoat/accordion.py +76 -0
- htmui/basecoat/alert.py +44 -0
- htmui/basecoat/avatar.py +52 -0
- htmui/basecoat/badge.py +32 -0
- htmui/basecoat/breadcrumb.py +46 -0
- htmui/basecoat/button.py +36 -0
- htmui/basecoat/button_group.py +22 -0
- htmui/basecoat/card.py +32 -0
- htmui/basecoat/cdn.py +14 -0
- htmui/basecoat/chart.py +86 -0
- htmui/basecoat/codeblock.py +109 -0
- htmui/basecoat/combobox.py +190 -0
- htmui/basecoat/command.py +128 -0
- htmui/basecoat/dialog.py +165 -0
- htmui/basecoat/drawer.py +66 -0
- htmui/basecoat/dropdown_menu.py +59 -0
- htmui/basecoat/field.py +91 -0
- htmui/basecoat/input_group.py +49 -0
- htmui/basecoat/item.py +94 -0
- htmui/basecoat/menu.py +131 -0
- htmui/basecoat/pagination.py +60 -0
- htmui/basecoat/popover.py +69 -0
- htmui/basecoat/progress.py +57 -0
- htmui/basecoat/radio_group.py +83 -0
- htmui/basecoat/select.py +190 -0
- htmui/basecoat/separator.py +8 -0
- htmui/basecoat/sidebar.py +270 -0
- htmui/basecoat/slider.py +50 -0
- htmui/basecoat/spinner.py +43 -0
- htmui/basecoat/switch.py +50 -0
- htmui/basecoat/table.py +38 -0
- htmui/basecoat/tabs.py +109 -0
- htmui/basecoat/theme_switcher.py +58 -0
- htmui/basecoat/toast.py +97 -0
- htmui/basecoat/typing.py +21 -0
- htmui/cli.py +257 -0
- htmui-0.1.0.dist-info/METADATA +159 -0
- htmui-0.1.0.dist-info/RECORD +43 -0
- htmui-0.1.0.dist-info/WHEEL +4 -0
- htmui-0.1.0.dist-info/entry_points.txt +5 -0
- htmui-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
|
|
4
|
+
from htmy import ComponentType, PropertyValue, SafeStr, XBool, html, join_classes
|
|
5
|
+
|
|
6
|
+
__version__ = "0.2.0"
|
|
7
|
+
__framework__ = "BasecoatUI"
|
|
8
|
+
__framework_version__ = "1"
|
|
9
|
+
__framework_url__ = "https://basecoatui.com/components/combobox/"
|
|
10
|
+
|
|
11
|
+
js = SafeStr(
|
|
12
|
+
'<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1/dist/js/combobox.min.js" defer></script>'
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
dropdown_icon = SafeStr(
|
|
16
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" '
|
|
17
|
+
'stroke="currentColor" class="size-5"><path stroke-linecap="round" stroke-linejoin="round" '
|
|
18
|
+
'd="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" /></svg>'
|
|
19
|
+
)
|
|
20
|
+
"""`chevron-up-down` icon from https://heroicons.com/."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def option(
|
|
24
|
+
*children: ComponentType,
|
|
25
|
+
value: str | None = None,
|
|
26
|
+
label: str | None = None,
|
|
27
|
+
filter: str | None = None,
|
|
28
|
+
force: bool = False,
|
|
29
|
+
disabled: bool = False,
|
|
30
|
+
keywords: str | None = None,
|
|
31
|
+
selected: bool = False,
|
|
32
|
+
**kwargs: PropertyValue,
|
|
33
|
+
) -> ComponentType:
|
|
34
|
+
"""
|
|
35
|
+
Search option for `combobox`.
|
|
36
|
+
|
|
37
|
+
Arguments:
|
|
38
|
+
value: The value submitted by the hidden input.
|
|
39
|
+
label: Optional label that overrides the option content in
|
|
40
|
+
the displayed input when the `option` is selected.
|
|
41
|
+
filter: Optional filter text.
|
|
42
|
+
force: When `true`, the option always stays visible.
|
|
43
|
+
disabled: Whether the option is disabled and non-selectable.
|
|
44
|
+
keywords: Optional keywords for filtering.
|
|
45
|
+
selected: Whether the option is initially selected.
|
|
46
|
+
**kwargs: Other attributes passed directly to the element.
|
|
47
|
+
"""
|
|
48
|
+
kwargs["role"] = "option"
|
|
49
|
+
if value is not None:
|
|
50
|
+
kwargs["data-value"] = value
|
|
51
|
+
if label is not None:
|
|
52
|
+
kwargs["data-label"] = label
|
|
53
|
+
if keywords is not None:
|
|
54
|
+
kwargs["data-keywords"] = keywords
|
|
55
|
+
if filter is not None:
|
|
56
|
+
kwargs["data-filter"] = filter
|
|
57
|
+
if force:
|
|
58
|
+
kwargs["data-force"] = "true"
|
|
59
|
+
if disabled:
|
|
60
|
+
kwargs["aria-disabled"] = "true"
|
|
61
|
+
if selected:
|
|
62
|
+
kwargs["aria-selected"] = "true"
|
|
63
|
+
return html.div(*children, **kwargs)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def group(
|
|
67
|
+
*items: ComponentType,
|
|
68
|
+
heading: ComponentType,
|
|
69
|
+
heading_id: str,
|
|
70
|
+
**kwargs: PropertyValue,
|
|
71
|
+
) -> ComponentType:
|
|
72
|
+
"""
|
|
73
|
+
`div` with `role="group"`, containing options with a linked heading.
|
|
74
|
+
|
|
75
|
+
Arguments:
|
|
76
|
+
heading: Group heading content.
|
|
77
|
+
heading_id: Id of the heading element; also used as `aria-labelledby`.
|
|
78
|
+
**kwargs: Other attributes passed through to the root element.
|
|
79
|
+
"""
|
|
80
|
+
kwargs["role"] = "group"
|
|
81
|
+
kwargs["aria-labelledby"] = heading_id
|
|
82
|
+
return html.div(
|
|
83
|
+
html.span(heading, role="heading", id=heading_id),
|
|
84
|
+
*items,
|
|
85
|
+
**kwargs,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def combobox( # noqa: C901
|
|
90
|
+
*items: ComponentType,
|
|
91
|
+
name: str,
|
|
92
|
+
value: str | list[Any] | None = None,
|
|
93
|
+
placeholder: str | None = None,
|
|
94
|
+
disabled: bool = False,
|
|
95
|
+
invalid: bool = False,
|
|
96
|
+
empty_message: str | None = None,
|
|
97
|
+
class_: str | None = None,
|
|
98
|
+
multi: bool = False,
|
|
99
|
+
auto_highlight: bool = False,
|
|
100
|
+
close_on_select: bool = False,
|
|
101
|
+
data_format: Literal["object"] | None = None,
|
|
102
|
+
**kwargs: PropertyValue,
|
|
103
|
+
) -> ComponentType:
|
|
104
|
+
"""
|
|
105
|
+
Combobox component root.
|
|
106
|
+
|
|
107
|
+
The component is input-first: the visible text input filters the list of
|
|
108
|
+
options, while a hidden input stores the value submitted under `name`.
|
|
109
|
+
|
|
110
|
+
Internal element IDs are derived from `name` automatically.
|
|
111
|
+
|
|
112
|
+
Arguments:
|
|
113
|
+
name: The name of the hidden input.
|
|
114
|
+
value: The value of the combobox.
|
|
115
|
+
placeholder: Placeholder text for the visible input.
|
|
116
|
+
disabled: Disables the combobox.
|
|
117
|
+
invalid: Mark the input as invalid for accessibility and validation.
|
|
118
|
+
empty_message: Optional message shown when the filter matches nothing.
|
|
119
|
+
class_: Additional classes for the root element.
|
|
120
|
+
multi: Whether multi-selection is enabled.
|
|
121
|
+
auto_highlight: Make the first visible option active when the list opens
|
|
122
|
+
or filters, so Enter can select it without arrowing first.
|
|
123
|
+
close_on_select: Whether to close the popover after selecting an option
|
|
124
|
+
in multi-selection mode.
|
|
125
|
+
data_format: Submitted value format. Use `"object"` to serialize selected
|
|
126
|
+
values as `{ value, label }` objects.
|
|
127
|
+
**kwargs: Extra attributes for the root element.
|
|
128
|
+
"""
|
|
129
|
+
root_id = f"{name}-root"
|
|
130
|
+
input_id = f"{root_id}-input"
|
|
131
|
+
popover_id = f"{root_id}-popover"
|
|
132
|
+
listbox_id = f"{root_id}-listbox"
|
|
133
|
+
|
|
134
|
+
if multi and close_on_select:
|
|
135
|
+
kwargs["data-close-on-select"] = "true"
|
|
136
|
+
if auto_highlight:
|
|
137
|
+
kwargs["data-auto-highlight"] = "true"
|
|
138
|
+
if data_format is not None:
|
|
139
|
+
kwargs["data-format"] = data_format
|
|
140
|
+
|
|
141
|
+
input_attrs: dict[str, PropertyValue] = {
|
|
142
|
+
"type": "text",
|
|
143
|
+
"role": "combobox",
|
|
144
|
+
"placeholder": placeholder,
|
|
145
|
+
"autocomplete": "off",
|
|
146
|
+
"autocorrect": "off",
|
|
147
|
+
"spellcheck": "false",
|
|
148
|
+
"aria-autocomplete": "list",
|
|
149
|
+
"aria-expanded": "false",
|
|
150
|
+
"aria-controls": listbox_id,
|
|
151
|
+
"id": input_id,
|
|
152
|
+
}
|
|
153
|
+
if disabled:
|
|
154
|
+
input_attrs["disabled"] = XBool.true
|
|
155
|
+
if invalid:
|
|
156
|
+
input_attrs["aria-invalid"] = "true"
|
|
157
|
+
|
|
158
|
+
listbox_attrs: dict[str, PropertyValue] = {
|
|
159
|
+
"role": "listbox",
|
|
160
|
+
"id": listbox_id,
|
|
161
|
+
"aria-labelledby": input_id,
|
|
162
|
+
"aria-orientation": "vertical",
|
|
163
|
+
}
|
|
164
|
+
if multi:
|
|
165
|
+
listbox_attrs["aria-multiselectable"] = "true"
|
|
166
|
+
if empty_message is not None:
|
|
167
|
+
listbox_attrs["data-empty"] = empty_message
|
|
168
|
+
|
|
169
|
+
effective_value: str
|
|
170
|
+
if value is None:
|
|
171
|
+
effective_value = "" if not multi else "[]"
|
|
172
|
+
elif isinstance(value, (list, tuple)):
|
|
173
|
+
effective_value = json.dumps(list(value))
|
|
174
|
+
else:
|
|
175
|
+
effective_value = str(value)
|
|
176
|
+
|
|
177
|
+
return html.div(
|
|
178
|
+
html.input_(**input_attrs),
|
|
179
|
+
dropdown_icon,
|
|
180
|
+
html.div(
|
|
181
|
+
html.div(*items, **listbox_attrs),
|
|
182
|
+
id=popover_id,
|
|
183
|
+
data_popover=XBool.true,
|
|
184
|
+
aria_hidden="true",
|
|
185
|
+
),
|
|
186
|
+
html.input_(type="hidden", name=name, value=effective_value),
|
|
187
|
+
id=root_id,
|
|
188
|
+
class_=join_classes("combobox", class_),
|
|
189
|
+
**kwargs,
|
|
190
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from htmy import ComponentType, PropertyValue, SafeStr, html, join_classes
|
|
4
|
+
|
|
5
|
+
from .menu import menu_item
|
|
6
|
+
|
|
7
|
+
__version__ = "0.2.0"
|
|
8
|
+
__framework__ = "BasecoatUI"
|
|
9
|
+
__framework_version__ = "1"
|
|
10
|
+
__framework_url__ = "https://basecoatui.com/components/command/"
|
|
11
|
+
|
|
12
|
+
js = SafeStr(
|
|
13
|
+
'<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1/dist/js/command.min.js" defer></script>'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
search_icon = SafeStr(
|
|
18
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" '
|
|
19
|
+
'stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" '
|
|
20
|
+
'd="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /></svg>'
|
|
21
|
+
)
|
|
22
|
+
"""`magnifying-glass` icon from https://heroicons.com/."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def command(
|
|
26
|
+
*items: ComponentType,
|
|
27
|
+
id: str,
|
|
28
|
+
input_icon: ComponentType = None,
|
|
29
|
+
input_placeholder: str = "Type a command or search...",
|
|
30
|
+
aria_label: str | None = "Command menu",
|
|
31
|
+
no_results_message: str = "No results found.",
|
|
32
|
+
class_: str | None = None,
|
|
33
|
+
**kwargs: PropertyValue,
|
|
34
|
+
) -> ComponentType:
|
|
35
|
+
"""Basecoat v1 command menu.
|
|
36
|
+
|
|
37
|
+
Items should be created with `menu_item(..., filter=..., keywords=...)` from
|
|
38
|
+
`htmui.basecoat.menu`. The `filter` text is required for the command JavaScript
|
|
39
|
+
to match the item while typing.
|
|
40
|
+
"""
|
|
41
|
+
menu_id = f"{id}-menu"
|
|
42
|
+
root_props: dict[str, Any] = {"id": id}
|
|
43
|
+
if aria_label is not None:
|
|
44
|
+
root_props["aria_label"] = aria_label
|
|
45
|
+
|
|
46
|
+
return html.div(
|
|
47
|
+
html.header(
|
|
48
|
+
input_icon,
|
|
49
|
+
html.input_(
|
|
50
|
+
type="text",
|
|
51
|
+
id=f"{id}-input",
|
|
52
|
+
role="combobox",
|
|
53
|
+
autocomplete="off",
|
|
54
|
+
autocorrect="off",
|
|
55
|
+
placeholder=input_placeholder,
|
|
56
|
+
spellcheck="false",
|
|
57
|
+
aria_autocomplete="list",
|
|
58
|
+
aria_expanded="true",
|
|
59
|
+
aria_controls=menu_id,
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
html.div(
|
|
63
|
+
*items,
|
|
64
|
+
id=menu_id,
|
|
65
|
+
role="menu",
|
|
66
|
+
aria_orientation="vertical",
|
|
67
|
+
data_empty=no_results_message,
|
|
68
|
+
),
|
|
69
|
+
class_=join_classes("command", class_),
|
|
70
|
+
**root_props,
|
|
71
|
+
**kwargs,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def command_item(
|
|
76
|
+
*children: ComponentType,
|
|
77
|
+
filter: str,
|
|
78
|
+
keywords: str | None = None,
|
|
79
|
+
shortcut: str | None = None,
|
|
80
|
+
indicator: ComponentType | None = None,
|
|
81
|
+
disabled: bool = False,
|
|
82
|
+
force: bool = False,
|
|
83
|
+
keep_open: bool = False,
|
|
84
|
+
class_: str | None = None,
|
|
85
|
+
**kwargs: PropertyValue,
|
|
86
|
+
) -> ComponentType:
|
|
87
|
+
"""Basecoat v1 command item (`filter` is required for the command JS to match it)."""
|
|
88
|
+
return menu_item(
|
|
89
|
+
*children,
|
|
90
|
+
filter=filter,
|
|
91
|
+
keywords=keywords,
|
|
92
|
+
shortcut=shortcut,
|
|
93
|
+
shortcut_kind="command",
|
|
94
|
+
indicator=indicator,
|
|
95
|
+
disabled=disabled,
|
|
96
|
+
force=force,
|
|
97
|
+
keep_open=keep_open,
|
|
98
|
+
class_=class_,
|
|
99
|
+
**kwargs, # type: ignore[arg-type]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def command_dialog(
|
|
104
|
+
*items: ComponentType,
|
|
105
|
+
id: str,
|
|
106
|
+
input_icon: ComponentType = None,
|
|
107
|
+
input_placeholder: str = "Type a command or search...",
|
|
108
|
+
aria_label: str | None = "Command menu",
|
|
109
|
+
no_results_message: str = "No results found.",
|
|
110
|
+
onclick: str | None = "if (event.target === this) this.close()",
|
|
111
|
+
**kwargs: PropertyValue,
|
|
112
|
+
) -> ComponentType:
|
|
113
|
+
"""Basecoat v1 command palette wrapped in `<dialog class="command-dialog">`."""
|
|
114
|
+
return html.dialog(
|
|
115
|
+
command(
|
|
116
|
+
*items,
|
|
117
|
+
id=f"{id}-command",
|
|
118
|
+
input_icon=input_icon,
|
|
119
|
+
input_placeholder=input_placeholder,
|
|
120
|
+
aria_label=None,
|
|
121
|
+
no_results_message=no_results_message,
|
|
122
|
+
),
|
|
123
|
+
id=id,
|
|
124
|
+
class_="command-dialog",
|
|
125
|
+
aria_label=aria_label,
|
|
126
|
+
onclick=onclick,
|
|
127
|
+
**kwargs,
|
|
128
|
+
)
|
htmui/basecoat/dialog.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from htmy import ComponentType, PropertyValue, SafeStr, html, join_classes
|
|
4
|
+
|
|
5
|
+
from .button import ButtonSize, ButtonVariant
|
|
6
|
+
from .typing import IdToProperty
|
|
7
|
+
|
|
8
|
+
__version__ = "0.2.0"
|
|
9
|
+
__framework__ = "BasecoatUI"
|
|
10
|
+
__framework_version__ = "1"
|
|
11
|
+
__framework_url__ = "https://basecoatui.com/components/dialog/"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class handle_close_button_click:
|
|
15
|
+
close: str = "this.closest('dialog').close()"
|
|
16
|
+
delete: str = "this.closest('dialog').delete()"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
close_icon = SafeStr(
|
|
20
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" '
|
|
21
|
+
'stroke-width="1.5" stroke="currentColor" class="size-5">'
|
|
22
|
+
'<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />'
|
|
23
|
+
"</svg>"
|
|
24
|
+
)
|
|
25
|
+
"""`x-mark` icon from https://heroicons.com/."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def close_button(
|
|
29
|
+
icon: ComponentType = close_icon,
|
|
30
|
+
*,
|
|
31
|
+
onclick: str = handle_close_button_click.close,
|
|
32
|
+
aria_label: str = "Close dialog",
|
|
33
|
+
class_: str | None = None,
|
|
34
|
+
size: ButtonSize | None = "icon-sm",
|
|
35
|
+
variant: ButtonVariant | None = "ghost",
|
|
36
|
+
**kwargs: PropertyValue,
|
|
37
|
+
) -> ComponentType:
|
|
38
|
+
"""Close button for dialogs."""
|
|
39
|
+
if variant is not None:
|
|
40
|
+
kwargs["data_variant"] = variant
|
|
41
|
+
if size is not None:
|
|
42
|
+
kwargs["data_size"] = size
|
|
43
|
+
return html.button(
|
|
44
|
+
icon,
|
|
45
|
+
type="button",
|
|
46
|
+
class_=join_classes("btn", class_),
|
|
47
|
+
onclick=onclick,
|
|
48
|
+
aria_label=aria_label,
|
|
49
|
+
**kwargs,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
AlertDialogSize = Literal["sm"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class handle_click:
|
|
57
|
+
close: str = "if (event.target === this) this.close()"
|
|
58
|
+
delete: str = "if (event.target === this) this.delete()"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def dialog(
|
|
62
|
+
*children: ComponentType,
|
|
63
|
+
id: str,
|
|
64
|
+
title: ComponentType,
|
|
65
|
+
description: ComponentType | None = None,
|
|
66
|
+
footer: ComponentType | None = None,
|
|
67
|
+
close_button: ComponentType | None = close_button(), # noqa: B008
|
|
68
|
+
figure: ComponentType | None = None,
|
|
69
|
+
alert: bool = False,
|
|
70
|
+
size: AlertDialogSize | None = None,
|
|
71
|
+
class_: str | None = None,
|
|
72
|
+
content_class: str | None = None,
|
|
73
|
+
onclick: str = handle_click.close,
|
|
74
|
+
**kwargs: PropertyValue,
|
|
75
|
+
) -> ComponentType:
|
|
76
|
+
"""
|
|
77
|
+
Dialog or alert-dialog modal.
|
|
78
|
+
|
|
79
|
+
Arguments:
|
|
80
|
+
*children: Dialog body content.
|
|
81
|
+
id: Dialog element id.
|
|
82
|
+
title: Dialog title.
|
|
83
|
+
description: Optional description shown under the title.
|
|
84
|
+
footer: Optional footer. Should be an `html.footer(...)` when provided.
|
|
85
|
+
close_button: Optional close button.
|
|
86
|
+
figure: Optional media to show in a `figure` in the header.
|
|
87
|
+
alert: Whether this is an alert dialog.
|
|
88
|
+
size: Dialog size.
|
|
89
|
+
class_: Extra CSS classes for the root `dialog` element.
|
|
90
|
+
content_class: Extra CSS classes for the inner content.
|
|
91
|
+
onclick: Click handler for the root `dialog` element. Ignored for alert dialogs.
|
|
92
|
+
**kwargs: Extra attributes for the `dialog` element.
|
|
93
|
+
"""
|
|
94
|
+
title_id = f"{id}-title"
|
|
95
|
+
description_id = f"{id}-description"
|
|
96
|
+
kwargs["aria_labelledby"] = title_id
|
|
97
|
+
if not alert:
|
|
98
|
+
kwargs["onclick"] = onclick
|
|
99
|
+
if size is not None:
|
|
100
|
+
kwargs["data_size"] = size
|
|
101
|
+
if description is not None:
|
|
102
|
+
kwargs["aria_describedby"] = description_id
|
|
103
|
+
|
|
104
|
+
return html.dialog(
|
|
105
|
+
html.div(
|
|
106
|
+
html.header(
|
|
107
|
+
html.figure(figure) if figure is not None else None,
|
|
108
|
+
html.h2(title, id=title_id),
|
|
109
|
+
html.p(description, id=description_id) if description is not None else None,
|
|
110
|
+
),
|
|
111
|
+
html.section(*children) if len(children) > 0 else None,
|
|
112
|
+
footer,
|
|
113
|
+
close_button,
|
|
114
|
+
class_=content_class,
|
|
115
|
+
),
|
|
116
|
+
id=id,
|
|
117
|
+
class_=join_classes("alert-dialog" if alert else "dialog", class_),
|
|
118
|
+
**kwargs,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class handle_show_button_click:
|
|
123
|
+
@staticmethod
|
|
124
|
+
def show_modal(dialog_id: str) -> PropertyValue:
|
|
125
|
+
return f"document.getElementById('{dialog_id}').showModal()"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def show_dialog_button(
|
|
129
|
+
title: ComponentType,
|
|
130
|
+
*,
|
|
131
|
+
dialog_id: str | None = None,
|
|
132
|
+
onclick: IdToProperty | None = handle_show_button_click.show_modal,
|
|
133
|
+
class_: str | None = None,
|
|
134
|
+
variant: ButtonVariant | None = "outline",
|
|
135
|
+
**kwargs: PropertyValue,
|
|
136
|
+
) -> ComponentType:
|
|
137
|
+
"""
|
|
138
|
+
Button that opens a dialog.
|
|
139
|
+
|
|
140
|
+
Arguments:
|
|
141
|
+
title: Button content.
|
|
142
|
+
dialog_id: Target dialog ID. Required when `onclick` is not `None`.
|
|
143
|
+
onclick: Optional callback that receives `dialog_id` and returns the value of
|
|
144
|
+
the `onclick` property. Defaults to `handle_show_button_click.show_modal()`.
|
|
145
|
+
class_: Extra CSS classes for the button.
|
|
146
|
+
variant: Button variant.
|
|
147
|
+
**kwargs: Extra attributes for the button.
|
|
148
|
+
"""
|
|
149
|
+
if onclick is not None:
|
|
150
|
+
if dialog_id is None:
|
|
151
|
+
raise ValueError("dialog_id is required when onclick is set")
|
|
152
|
+
|
|
153
|
+
click = onclick(dialog_id)
|
|
154
|
+
if click is not None:
|
|
155
|
+
kwargs["onclick"] = click
|
|
156
|
+
|
|
157
|
+
if variant is not None:
|
|
158
|
+
kwargs["data_variant"] = variant
|
|
159
|
+
|
|
160
|
+
return html.button(
|
|
161
|
+
title,
|
|
162
|
+
class_=join_classes("btn", class_),
|
|
163
|
+
type="button",
|
|
164
|
+
**kwargs,
|
|
165
|
+
)
|
htmui/basecoat/drawer.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from htmy import ComponentType, PropertyValue, SafeStr, html, join_classes
|
|
2
|
+
|
|
3
|
+
from .typing import Side
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.0"
|
|
6
|
+
__framework__ = "BasecoatUI"
|
|
7
|
+
__framework_version__ = "1"
|
|
8
|
+
__framework_url__ = "https://basecoatui.com/components/drawer/"
|
|
9
|
+
|
|
10
|
+
js = SafeStr(
|
|
11
|
+
'<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1/dist/js/drawer.min.js" defer></script>'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def drawer(
|
|
16
|
+
*children: ComponentType,
|
|
17
|
+
id: str,
|
|
18
|
+
title: ComponentType | None = None,
|
|
19
|
+
description: ComponentType | None = None,
|
|
20
|
+
footer: ComponentType | None = None,
|
|
21
|
+
side: Side | None = None,
|
|
22
|
+
class_: str | None = None,
|
|
23
|
+
content_class: str | None = None,
|
|
24
|
+
**kwargs: PropertyValue,
|
|
25
|
+
) -> ComponentType:
|
|
26
|
+
"""
|
|
27
|
+
Drawer component.
|
|
28
|
+
|
|
29
|
+
Arguments:
|
|
30
|
+
*children: Drawer body content.
|
|
31
|
+
id: Drawer element ID.
|
|
32
|
+
title: Optional title.
|
|
33
|
+
description: Optional description shown under the title.
|
|
34
|
+
footer: Optional footer. Should be an `html.footer()` when provided.
|
|
35
|
+
side: On which side the drawer should appear.
|
|
36
|
+
class_: Extra CSS classes for the root element.
|
|
37
|
+
content_class: Extra CSS classes for the inner content.
|
|
38
|
+
**kwargs: Extra attributes for the root element.
|
|
39
|
+
"""
|
|
40
|
+
title_id = f"{id}-title"
|
|
41
|
+
description_id = f"{id}-description"
|
|
42
|
+
if title is not None:
|
|
43
|
+
kwargs["aria_labelledby"] = title_id
|
|
44
|
+
if description is not None:
|
|
45
|
+
kwargs["aria_describedby"] = description_id
|
|
46
|
+
if side is not None:
|
|
47
|
+
kwargs["data_side"] = side
|
|
48
|
+
|
|
49
|
+
header = None
|
|
50
|
+
if title is not None or description is not None:
|
|
51
|
+
header = html.header(
|
|
52
|
+
html.h2(title, id=title_id) if title is not None else None,
|
|
53
|
+
html.p(description, id=description_id) if description is not None else None,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return html.dialog(
|
|
57
|
+
html.article(
|
|
58
|
+
header,
|
|
59
|
+
html.section(*children) if len(children) > 0 else None,
|
|
60
|
+
footer,
|
|
61
|
+
class_=content_class,
|
|
62
|
+
),
|
|
63
|
+
id=id,
|
|
64
|
+
class_=join_classes("drawer", class_),
|
|
65
|
+
**kwargs,
|
|
66
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from htmy import ComponentType, PropertyValue, SafeStr, html, join_classes
|
|
2
|
+
|
|
3
|
+
from .button import ButtonVariant
|
|
4
|
+
from .typing import Align, FloatingSide
|
|
5
|
+
|
|
6
|
+
__version__ = "0.2.0"
|
|
7
|
+
__framework__ = "BasecoatUI"
|
|
8
|
+
__framework_version__ = "1"
|
|
9
|
+
__framework_url__ = "https://basecoatui.com/components/dropdown-menu/"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
js = SafeStr(
|
|
13
|
+
'<script src="https://cdn.jsdelivr.net/npm/basecoat-css@1/dist/js/dropdown-menu.min.js" defer></script>'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dropdown_menu(
|
|
18
|
+
*items: ComponentType,
|
|
19
|
+
id: str,
|
|
20
|
+
button_content: ComponentType,
|
|
21
|
+
button_class: str | None = None,
|
|
22
|
+
button_variant: ButtonVariant | None = "outline",
|
|
23
|
+
class_: str | None = None,
|
|
24
|
+
popover_class: str | None = None,
|
|
25
|
+
popover_align: Align | None = None,
|
|
26
|
+
popover_side: FloatingSide | None = None,
|
|
27
|
+
**kwargs: PropertyValue,
|
|
28
|
+
) -> ComponentType:
|
|
29
|
+
menu_id = f"{id}-menu"
|
|
30
|
+
button_id = f"{menu_id}-button"
|
|
31
|
+
|
|
32
|
+
return html.div(
|
|
33
|
+
html.button(
|
|
34
|
+
button_content,
|
|
35
|
+
type="button",
|
|
36
|
+
id=button_id,
|
|
37
|
+
aria_controls=menu_id,
|
|
38
|
+
aria_expanded="false",
|
|
39
|
+
aria_haspopup="menu",
|
|
40
|
+
class_=join_classes("btn", button_class),
|
|
41
|
+
**({"data_variant": button_variant} if button_variant else {}),
|
|
42
|
+
),
|
|
43
|
+
html.div(
|
|
44
|
+
html.div(
|
|
45
|
+
*items,
|
|
46
|
+
id=menu_id,
|
|
47
|
+
role="menu",
|
|
48
|
+
aria_labelledby=button_id,
|
|
49
|
+
),
|
|
50
|
+
data_popover="",
|
|
51
|
+
aria_hidden="true",
|
|
52
|
+
class_="min-w-56" if popover_class is None else popover_class,
|
|
53
|
+
data_align=popover_align,
|
|
54
|
+
data_side=popover_side,
|
|
55
|
+
),
|
|
56
|
+
class_=join_classes("dropdown-menu", class_),
|
|
57
|
+
id=id,
|
|
58
|
+
**kwargs,
|
|
59
|
+
)
|
htmui/basecoat/field.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from htmy import ComponentType, PropertyValue, html, join_classes
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.0"
|
|
6
|
+
__framework__ = "BasecoatUI"
|
|
7
|
+
__framework_version__ = "1"
|
|
8
|
+
__framework_url__ = "https://basecoatui.com/components/field/"
|
|
9
|
+
|
|
10
|
+
FieldOrientation = Literal["horizontal", "responsive"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fieldset(
|
|
14
|
+
*children: ComponentType,
|
|
15
|
+
title: ComponentType | None = None,
|
|
16
|
+
subtitle: ComponentType | None = None,
|
|
17
|
+
class_: str | None = None,
|
|
18
|
+
title_class: str | None = None,
|
|
19
|
+
**kwargs: PropertyValue,
|
|
20
|
+
) -> ComponentType:
|
|
21
|
+
"""
|
|
22
|
+
Fieldset that groups related fields.
|
|
23
|
+
|
|
24
|
+
Arguments:
|
|
25
|
+
*children: Fields and other content inside the fieldset.
|
|
26
|
+
title: Optional title.
|
|
27
|
+
subtitle: Optional description, shown after the title.
|
|
28
|
+
class_: Extra CSS classes for the root element.
|
|
29
|
+
title_class: Extra CSS classes for the title.
|
|
30
|
+
**kwargs: Extra attributes for the root element.
|
|
31
|
+
"""
|
|
32
|
+
return html.fieldset(
|
|
33
|
+
html.legend(title, class_=title_class) if title is not None else None,
|
|
34
|
+
subtitle,
|
|
35
|
+
*children,
|
|
36
|
+
class_=join_classes("fieldset", class_),
|
|
37
|
+
**kwargs,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def field(
|
|
42
|
+
*children: ComponentType,
|
|
43
|
+
orientation: FieldOrientation | None = None,
|
|
44
|
+
invalid: bool = False,
|
|
45
|
+
disabled: bool = False,
|
|
46
|
+
class_: str | None = None,
|
|
47
|
+
**kwargs: PropertyValue,
|
|
48
|
+
) -> ComponentType:
|
|
49
|
+
"""
|
|
50
|
+
Single field.
|
|
51
|
+
|
|
52
|
+
Arguments:
|
|
53
|
+
*children: Field content (label, control, description, etc.).
|
|
54
|
+
orientation: Layout orientation.
|
|
55
|
+
invalid: Whether to mark the field as invalid.
|
|
56
|
+
disabled: Whether to mark the field as disabled.
|
|
57
|
+
class_: Extra CSS classes for the root element.
|
|
58
|
+
**kwargs: Extra attributes for the root element.
|
|
59
|
+
"""
|
|
60
|
+
if orientation is not None:
|
|
61
|
+
kwargs["data_orientation"] = orientation
|
|
62
|
+
if invalid:
|
|
63
|
+
kwargs["data_invalid"] = "true"
|
|
64
|
+
if disabled:
|
|
65
|
+
kwargs["data_disabled"] = "true"
|
|
66
|
+
|
|
67
|
+
return html.div(*children, class_=join_classes("field", class_), role="group", **kwargs)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def field_separator(
|
|
71
|
+
label: ComponentType | None = None,
|
|
72
|
+
*,
|
|
73
|
+
class_: str | None = None,
|
|
74
|
+
label_class: str | None = None,
|
|
75
|
+
**kwargs: PropertyValue,
|
|
76
|
+
) -> ComponentType:
|
|
77
|
+
"""
|
|
78
|
+
Separator between field groups.
|
|
79
|
+
|
|
80
|
+
Arguments:
|
|
81
|
+
label: Optional label rendered over the separator.
|
|
82
|
+
class_: Extra CSS classes for the root element.
|
|
83
|
+
label_class: Extra CSS classes for the label element.
|
|
84
|
+
**kwargs: Extra attributes for the root element.
|
|
85
|
+
"""
|
|
86
|
+
return html.div(
|
|
87
|
+
html.hr(role="separator"),
|
|
88
|
+
html.span(label, class_=label_class) if label is not None else None,
|
|
89
|
+
class_=join_classes("field-separator", class_),
|
|
90
|
+
**kwargs,
|
|
91
|
+
)
|