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.
@@ -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
+ )
@@ -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
+ )
@@ -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
+ )
@@ -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
+ )