nicegui-bootstrap-components 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.
Files changed (103) hide show
  1. nicegui_bootstrap_components/__init__.py +7 -0
  2. nicegui_bootstrap_components/_base.py +590 -0
  3. nicegui_bootstrap_components/_host.py +259 -0
  4. nicegui_bootstrap_components/assets.py +371 -0
  5. nicegui_bootstrap_components/bs/__init__.py +267 -0
  6. nicegui_bootstrap_components/bs/_actions.py +232 -0
  7. nicegui_bootstrap_components/bs/_alert_spinner.py +423 -0
  8. nicegui_bootstrap_components/bs/_carousel.py +312 -0
  9. nicegui_bootstrap_components/bs/_choices.py +779 -0
  10. nicegui_bootstrap_components/bs/_content.py +1343 -0
  11. nicegui_bootstrap_components/bs/_forms.py +378 -0
  12. nicegui_bootstrap_components/bs/_inputs.py +560 -0
  13. nicegui_bootstrap_components/bs/_layout.py +341 -0
  14. nicegui_bootstrap_components/bs/_nav.py +650 -0
  15. nicegui_bootstrap_components/bs/_navbar.py +445 -0
  16. nicegui_bootstrap_components/bs/_overlay_root.py +73 -0
  17. nicegui_bootstrap_components/bs/_overlays.py +2633 -0
  18. nicegui_bootstrap_components/bs/_scope.py +28 -0
  19. nicegui_bootstrap_components/bs/_tabs.py +605 -0
  20. nicegui_bootstrap_components/bs/_toast.py +137 -0
  21. nicegui_bootstrap_components/bs/_tooltip_popover.py +537 -0
  22. nicegui_bootstrap_components/contracts/dbc-2.0.4.json +16811 -0
  23. nicegui_bootstrap_components/dbc/__init__.py +108 -0
  24. nicegui_bootstrap_components/icons.py +29 -0
  25. nicegui_bootstrap_components/persist.py +201 -0
  26. nicegui_bootstrap_components/py.typed +0 -0
  27. nicegui_bootstrap_components/static/dist/fonts/bootstrap-icons.woff +0 -0
  28. nicegui_bootstrap_components/static/dist/fonts/bootstrap-icons.woff2 +0 -0
  29. nicegui_bootstrap_components/static/dist/ngbs-bootstrap-unscoped.css +12047 -0
  30. nicegui_bootstrap_components/static/dist/ngbs-bootstrap.css +12057 -0
  31. nicegui_bootstrap_components/static/dist/ngbs-cerulean-unscoped.css +12338 -0
  32. nicegui_bootstrap_components/static/dist/ngbs-cerulean.css +12348 -0
  33. nicegui_bootstrap_components/static/dist/ngbs-compat-unscoped.css +12 -0
  34. nicegui_bootstrap_components/static/dist/ngbs-compat.css +12 -0
  35. nicegui_bootstrap_components/static/dist/ngbs-cosmo-unscoped.css +11825 -0
  36. nicegui_bootstrap_components/static/dist/ngbs-cosmo.css +11835 -0
  37. nicegui_bootstrap_components/static/dist/ngbs-cyborg-unscoped.css +12213 -0
  38. nicegui_bootstrap_components/static/dist/ngbs-cyborg.css +12223 -0
  39. nicegui_bootstrap_components/static/dist/ngbs-darkly-unscoped.css +12143 -0
  40. nicegui_bootstrap_components/static/dist/ngbs-darkly.css +12153 -0
  41. nicegui_bootstrap_components/static/dist/ngbs-flatly-unscoped.css +12129 -0
  42. nicegui_bootstrap_components/static/dist/ngbs-flatly.css +12139 -0
  43. nicegui_bootstrap_components/static/dist/ngbs-grid-unscoped.css +4084 -0
  44. nicegui_bootstrap_components/static/dist/ngbs-grid.css +4094 -0
  45. nicegui_bootstrap_components/static/dist/ngbs-host-unscoped.css +9 -0
  46. nicegui_bootstrap_components/static/dist/ngbs-host.css +9 -0
  47. nicegui_bootstrap_components/static/dist/ngbs-icons-bootstrap.css +5 -0
  48. nicegui_bootstrap_components/static/dist/ngbs-icons-fontawesome.css +9 -0
  49. nicegui_bootstrap_components/static/dist/ngbs-journal-unscoped.css +12112 -0
  50. nicegui_bootstrap_components/static/dist/ngbs-journal.css +12122 -0
  51. nicegui_bootstrap_components/static/dist/ngbs-litera-unscoped.css +12214 -0
  52. nicegui_bootstrap_components/static/dist/ngbs-litera.css +12224 -0
  53. nicegui_bootstrap_components/static/dist/ngbs-lumen-unscoped.css +12377 -0
  54. nicegui_bootstrap_components/static/dist/ngbs-lumen.css +12387 -0
  55. nicegui_bootstrap_components/static/dist/ngbs-lux-unscoped.css +12048 -0
  56. nicegui_bootstrap_components/static/dist/ngbs-lux.css +12058 -0
  57. nicegui_bootstrap_components/static/dist/ngbs-materia-unscoped.css +13197 -0
  58. nicegui_bootstrap_components/static/dist/ngbs-materia.css +13207 -0
  59. nicegui_bootstrap_components/static/dist/ngbs-minty-unscoped.css +12184 -0
  60. nicegui_bootstrap_components/static/dist/ngbs-minty.css +12194 -0
  61. nicegui_bootstrap_components/static/dist/ngbs-morph-unscoped.css +12829 -0
  62. nicegui_bootstrap_components/static/dist/ngbs-morph.css +12839 -0
  63. nicegui_bootstrap_components/static/dist/ngbs-pulse-unscoped.css +11901 -0
  64. nicegui_bootstrap_components/static/dist/ngbs-pulse.css +11911 -0
  65. nicegui_bootstrap_components/static/dist/ngbs-quartz-unscoped.css +12625 -0
  66. nicegui_bootstrap_components/static/dist/ngbs-quartz.css +12635 -0
  67. nicegui_bootstrap_components/static/dist/ngbs-sandstone-unscoped.css +12214 -0
  68. nicegui_bootstrap_components/static/dist/ngbs-sandstone.css +12224 -0
  69. nicegui_bootstrap_components/static/dist/ngbs-simplex-unscoped.css +12199 -0
  70. nicegui_bootstrap_components/static/dist/ngbs-simplex.css +12209 -0
  71. nicegui_bootstrap_components/static/dist/ngbs-sketchy-unscoped.css +12518 -0
  72. nicegui_bootstrap_components/static/dist/ngbs-sketchy.css +12528 -0
  73. nicegui_bootstrap_components/static/dist/ngbs-slate-unscoped.css +12527 -0
  74. nicegui_bootstrap_components/static/dist/ngbs-slate.css +12537 -0
  75. nicegui_bootstrap_components/static/dist/ngbs-solar-unscoped.css +12152 -0
  76. nicegui_bootstrap_components/static/dist/ngbs-solar.css +12162 -0
  77. nicegui_bootstrap_components/static/dist/ngbs-spacelab-unscoped.css +12279 -0
  78. nicegui_bootstrap_components/static/dist/ngbs-spacelab.css +12289 -0
  79. nicegui_bootstrap_components/static/dist/ngbs-superhero-unscoped.css +12223 -0
  80. nicegui_bootstrap_components/static/dist/ngbs-superhero.css +12233 -0
  81. nicegui_bootstrap_components/static/dist/ngbs-united-unscoped.css +12090 -0
  82. nicegui_bootstrap_components/static/dist/ngbs-united.css +12100 -0
  83. nicegui_bootstrap_components/static/dist/ngbs-vapor-unscoped.css +12556 -0
  84. nicegui_bootstrap_components/static/dist/ngbs-vapor.css +12566 -0
  85. nicegui_bootstrap_components/static/dist/ngbs-yeti-unscoped.css +12333 -0
  86. nicegui_bootstrap_components/static/dist/ngbs-yeti.css +12343 -0
  87. nicegui_bootstrap_components/static/dist/ngbs-zephyr-unscoped.css +12296 -0
  88. nicegui_bootstrap_components/static/dist/ngbs-zephyr.css +12306 -0
  89. nicegui_bootstrap_components/static/prototype-ngbs.css +541 -0
  90. nicegui_bootstrap_components/static/webfonts/fa-brands-400.ttf +0 -0
  91. nicegui_bootstrap_components/static/webfonts/fa-brands-400.woff2 +0 -0
  92. nicegui_bootstrap_components/static/webfonts/fa-regular-400.ttf +0 -0
  93. nicegui_bootstrap_components/static/webfonts/fa-regular-400.woff2 +0 -0
  94. nicegui_bootstrap_components/static/webfonts/fa-solid-900.ttf +0 -0
  95. nicegui_bootstrap_components/static/webfonts/fa-solid-900.woff2 +0 -0
  96. nicegui_bootstrap_components/static/webfonts/fa-v4compatibility.ttf +0 -0
  97. nicegui_bootstrap_components/static/webfonts/fa-v4compatibility.woff2 +0 -0
  98. nicegui_bootstrap_components/theme.py +238 -0
  99. nicegui_bootstrap_components/themes.py +131 -0
  100. nicegui_bootstrap_components-0.1.0.dist-info/METADATA +234 -0
  101. nicegui_bootstrap_components-0.1.0.dist-info/RECORD +103 -0
  102. nicegui_bootstrap_components-0.1.0.dist-info/WHEEL +4 -0
  103. nicegui_bootstrap_components-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,779 @@
1
+ """Choice family: Select, Checkbox, Switch, RadioButton, Checklist, RadioItems."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ from nicegui.element import Element
9
+
10
+ from .._base import (
11
+ BootstrapValueElement,
12
+ ChildrenError,
13
+ PropConflictError,
14
+ make_surface_classes,
15
+ normalize_style,
16
+ style_to_css,
17
+ )
18
+ from .._host import set_element_text
19
+
20
+ __all__ = [
21
+ "Select",
22
+ "DbcSelect",
23
+ "select",
24
+ "Checkbox",
25
+ "DbcCheckbox",
26
+ "checkbox",
27
+ "RadioButton",
28
+ "DbcRadioButton",
29
+ "radio_button",
30
+ "Switch",
31
+ "DbcSwitch",
32
+ "switch",
33
+ "Checklist",
34
+ "DbcChecklist",
35
+ "checklist",
36
+ "RadioItems",
37
+ "DbcRadioItems",
38
+ "radio_items",
39
+ "normalize_choice_options",
40
+ "select_structural_classes",
41
+ "form_check_item_classes",
42
+ "choice_input_classes",
43
+ "choice_label_classes",
44
+ ]
45
+
46
+ _SELECT_SIZES = {"sm", "md", "lg"}
47
+
48
+ _JS_SELECT_CHANGE = """(e) => {
49
+ const root = e.currentTarget || e.target;
50
+ let sel = e.target;
51
+ if (!sel || sel.tagName !== 'SELECT') {
52
+ sel = root && root.tagName === 'SELECT' ? root : sel;
53
+ }
54
+ if (!sel || sel.tagName !== 'SELECT') return;
55
+ emit(sel.value == null ? '' : String(sel.value));
56
+ }"""
57
+
58
+ _JS_CHECK_CHANGE = """(e) => {
59
+ const root = e.currentTarget || e.target;
60
+ let input = e.target;
61
+ if (!input || input.tagName !== 'INPUT') {
62
+ input = root && root.querySelector ? root.querySelector('input') : input;
63
+ }
64
+ if (!input) return;
65
+ emit(!!input.checked);
66
+ }"""
67
+
68
+ _JS_CHECKLIST_CHANGE = """(e) => {
69
+ const root = e.currentTarget || e.target;
70
+ if (!root || !root.querySelectorAll) return;
71
+ const checked = [];
72
+ root.querySelectorAll('input[type="checkbox"]').forEach((el) => {
73
+ if (el.checked) checked.push(el.value == null ? '' : String(el.value));
74
+ });
75
+ emit(checked);
76
+ }"""
77
+
78
+ _JS_RADIO_ITEMS_CHANGE = """(e) => {
79
+ const root = e.currentTarget || e.target;
80
+ if (!root || !root.querySelector) return;
81
+ const selected = root.querySelector('input[type="radio"]:checked');
82
+ emit(selected && selected.value != null ? String(selected.value) : null);
83
+ }"""
84
+
85
+
86
+ def _split_classes(value: str | None) -> list[str]:
87
+ if not value:
88
+ return []
89
+ return [part for part in value.split() if part]
90
+
91
+
92
+ def _resolve_alias(primary: Any, fallback: Any) -> Any:
93
+ if primary is not None:
94
+ return primary
95
+ return fallback
96
+
97
+
98
+ def _html_value(value: Any) -> str:
99
+ if value is None:
100
+ return ""
101
+ return str(value)
102
+
103
+
104
+ def _payload_from_event(e: Any) -> Any:
105
+ return getattr(e, "args", e)
106
+
107
+
108
+ def _set_flag(element: Any, name: str, value: bool) -> None:
109
+ props = element._props
110
+ if value:
111
+ props[name] = True
112
+ else:
113
+ props.pop(name, None)
114
+
115
+
116
+ def _apply_style_dict(element: Element, style: dict[str, Any] | str | None) -> None:
117
+ if not style:
118
+ return
119
+ if isinstance(style, str):
120
+ element.style(style)
121
+ return
122
+ normalized = normalize_style(style)
123
+ if isinstance(normalized, str):
124
+ if normalized:
125
+ element.style(normalized)
126
+ return
127
+ css = style_to_css(normalized)
128
+ if css:
129
+ element.style(css)
130
+
131
+
132
+ def _has_children(children: object) -> bool:
133
+ if children is None:
134
+ return False
135
+ return not (children == [] or children == ())
136
+
137
+
138
+ def _normalize_one_option(item: Any) -> dict[str, Any]:
139
+ if isinstance(item, dict):
140
+ if "value" not in item and "label" not in item:
141
+ raise ValueError("Option dict requires 'label' or 'value'")
142
+ value = item["value"] if "value" in item else item["label"]
143
+ raw_label = item.get("label", value)
144
+ title = item.get("title")
145
+ return {
146
+ "value": value,
147
+ "label": "" if raw_label is None else str(raw_label),
148
+ "disabled": bool(item.get("disabled", False)),
149
+ "title": None if title is None else str(title),
150
+ "input_id": item.get("input_id"),
151
+ "label_id": item.get("label_id"),
152
+ }
153
+ if isinstance(item, (list, tuple)):
154
+ raise ValueError(f"Invalid option {item!r}; expected a string, number, or dict")
155
+ return {
156
+ "value": item,
157
+ "label": "" if item is None else str(item),
158
+ "disabled": False,
159
+ "title": None,
160
+ "input_id": None,
161
+ "label_id": None,
162
+ }
163
+
164
+
165
+ def normalize_choice_options(
166
+ options: list[Any] | dict[Any, Any] | tuple[Any, ...] | None,
167
+ ) -> list[dict[str, Any]]:
168
+ """Normalize choice options to dicts with label, value, disabled, title, and ids."""
169
+ if options is None:
170
+ return []
171
+ if isinstance(options, dict):
172
+ items = [{"value": key, "label": val} for key, val in options.items()]
173
+ return [_normalize_one_option(item) for item in items]
174
+ if isinstance(options, (list, tuple)):
175
+ return [_normalize_one_option(item) for item in options]
176
+ raise ValueError(f"options must be a list, tuple, or dict, not {type(options).__name__}")
177
+
178
+
179
+ def select_structural_classes(
180
+ *,
181
+ size: str | None = None,
182
+ valid: bool | None = None,
183
+ invalid: bool | None = None,
184
+ ) -> list[str]:
185
+ """Return Bootstrap ``form-select`` classes. Unknown sizes raise ``ValueError``."""
186
+ if size is not None and size not in _SELECT_SIZES:
187
+ raise ValueError(f"Invalid Select size {size!r}; expected one of 'sm', 'md', 'lg'")
188
+ if valid and invalid:
189
+ raise PropConflictError("valid", "invalid")
190
+ classes = ["form-select"]
191
+ if size in {"sm", "lg"}:
192
+ classes.append(f"form-select-{size}")
193
+ if valid:
194
+ classes.append("is-valid")
195
+ if invalid:
196
+ classes.append("is-invalid")
197
+ return classes
198
+
199
+
200
+ def form_check_item_classes(*, inline: bool = False, switch: bool = False) -> list[str]:
201
+ """Return Bootstrap ``form-check`` wrapper classes for a choice item."""
202
+ classes = ["form-check"]
203
+ if inline:
204
+ classes.append("form-check-inline")
205
+ if switch:
206
+ classes.append("form-switch")
207
+ return classes
208
+
209
+
210
+ def choice_input_classes(
211
+ *,
212
+ input_class_name: str | None = None,
213
+ input_checked_class_name: str | None = None,
214
+ checked: bool = False,
215
+ ) -> list[str]:
216
+ """Return classes for a ``form-check-input`` element."""
217
+ classes = ["form-check-input"]
218
+ classes.extend(_split_classes(input_class_name))
219
+ if checked:
220
+ classes.extend(_split_classes(input_checked_class_name))
221
+ return classes
222
+
223
+
224
+ def choice_label_classes(
225
+ *,
226
+ label_class_name: str | None = None,
227
+ label_checked_class_name: str | None = None,
228
+ checked: bool = False,
229
+ ) -> list[str]:
230
+ """Return classes for a ``form-check-label`` element."""
231
+ classes = ["form-check-label"]
232
+ classes.extend(_split_classes(label_class_name))
233
+ if checked:
234
+ classes.extend(_split_classes(label_checked_class_name))
235
+ return classes
236
+
237
+
238
+ def _option_is_selected(option_value: Any, current: Any, *, multi: bool) -> bool:
239
+ if multi:
240
+ if current is None:
241
+ return False
242
+ for item in current:
243
+ if item == option_value or _html_value(item) == _html_value(option_value):
244
+ return True
245
+ return False
246
+ if current is None:
247
+ return False
248
+ return current == option_value or _html_value(current) == _html_value(option_value)
249
+
250
+
251
+ def _match_option_value(value: Any, normalized: list[dict[str, Any]]) -> Any | None:
252
+ for opt in normalized:
253
+ if opt["value"] == value or _html_value(opt["value"]) == _html_value(value):
254
+ return opt["value"]
255
+ return None
256
+
257
+
258
+ def _filter_selected(values: list[Any], normalized: list[dict[str, Any]]) -> list[Any]:
259
+ filtered: list[Any] = []
260
+ seen: set[int] = set()
261
+ for item in values:
262
+ for index, opt in enumerate(normalized):
263
+ if opt["value"] == item or _html_value(opt["value"]) == _html_value(item):
264
+ if index not in seen:
265
+ filtered.append(opt["value"])
266
+ seen.add(index)
267
+ break
268
+ return filtered
269
+
270
+
271
+ def _make_option_element(
272
+ *,
273
+ value: str,
274
+ label: str,
275
+ disabled: bool = False,
276
+ title: str | None = None,
277
+ selected: bool = False,
278
+ hidden: bool = False,
279
+ ) -> Element:
280
+ element = Element("option")
281
+ element._props["value"] = value
282
+ set_element_text(element, label)
283
+ if disabled:
284
+ element._props["disabled"] = True
285
+ if selected:
286
+ element._props["selected"] = True
287
+ if hidden:
288
+ element._props["hidden"] = True
289
+ if title:
290
+ element._props["title"] = title
291
+ return element
292
+
293
+
294
+ def _mount_form_check(
295
+ parent: BootstrapValueElement,
296
+ *,
297
+ input_type: str,
298
+ checked: bool,
299
+ disabled: bool,
300
+ label: str | None,
301
+ name: str | None,
302
+ input_id: str | None,
303
+ label_id: str | None,
304
+ input_class_name: str | None,
305
+ input_style: dict[str, Any] | str | None,
306
+ label_class_name: str | None,
307
+ label_style: dict[str, Any] | str | None,
308
+ ) -> tuple[Element, Element]:
309
+ with parent:
310
+ input_el = Element("input")
311
+ input_el._classes.extend(
312
+ choice_input_classes(input_class_name=input_class_name, checked=checked)
313
+ )
314
+ input_el._props["type"] = input_type
315
+ if checked:
316
+ input_el._props["checked"] = True
317
+ if disabled:
318
+ input_el._props["disabled"] = True
319
+ if name is not None:
320
+ input_el._props["name"] = name
321
+ if input_id is not None:
322
+ input_el._props["id"] = input_id
323
+ _apply_style_dict(input_el, input_style)
324
+ label_el = Element("label")
325
+ label_el._classes.extend(
326
+ choice_label_classes(label_class_name=label_class_name, checked=checked)
327
+ )
328
+ set_element_text(label_el, "" if label is None else label)
329
+ if label_id is not None:
330
+ label_el._props["id"] = label_id
331
+ if input_id is not None:
332
+ label_el._props["for"] = input_id
333
+ _apply_style_dict(label_el, label_style)
334
+ return input_el, label_el
335
+
336
+
337
+ def _merged_style(
338
+ base: dict[str, Any] | str | None,
339
+ extra: dict[str, Any] | str | None,
340
+ *,
341
+ use_extra: bool,
342
+ ) -> dict[str, Any] | str | None:
343
+ if not use_extra or extra is None:
344
+ return base
345
+ if base is None:
346
+ return extra
347
+ if isinstance(base, dict) and isinstance(extra, dict):
348
+ merged = dict(base)
349
+ merged.update(extra)
350
+ return merged
351
+ return extra
352
+
353
+
354
+ def _mount_choice_item(
355
+ parent: BootstrapValueElement,
356
+ *,
357
+ input_type: str,
358
+ option: dict[str, Any],
359
+ checked: bool,
360
+ inline: bool,
361
+ switch: bool,
362
+ name: str | None,
363
+ input_class_name: str | None,
364
+ input_checked_class_name: str | None,
365
+ input_style: dict[str, Any] | str | None,
366
+ input_checked_style: dict[str, Any] | str | None,
367
+ label_class_name: str | None,
368
+ label_checked_class_name: str | None,
369
+ label_style: dict[str, Any] | str | None,
370
+ label_checked_style: dict[str, Any] | str | None,
371
+ input_id: str | None,
372
+ ) -> tuple[Element, Element, Element]:
373
+ with parent:
374
+ wrapper = Element("div")
375
+ wrapper._classes.extend(form_check_item_classes(inline=inline, switch=switch))
376
+ with wrapper:
377
+ input_el = Element("input")
378
+ input_el._classes.extend(
379
+ choice_input_classes(
380
+ input_class_name=input_class_name,
381
+ input_checked_class_name=input_checked_class_name,
382
+ checked=checked,
383
+ )
384
+ )
385
+ input_el._props["type"] = input_type
386
+ input_el._props["value"] = _html_value(option["value"])
387
+ if checked:
388
+ input_el._props["checked"] = True
389
+ if option["disabled"]:
390
+ input_el._props["disabled"] = True
391
+ if name is not None:
392
+ input_el._props["name"] = name
393
+ if input_id is not None:
394
+ input_el._props["id"] = input_id
395
+ _apply_style_dict(
396
+ input_el,
397
+ _merged_style(input_style, input_checked_style, use_extra=checked),
398
+ )
399
+ label_el = Element("label")
400
+ label_el._classes.extend(
401
+ choice_label_classes(
402
+ label_class_name=label_class_name,
403
+ label_checked_class_name=label_checked_class_name,
404
+ checked=checked,
405
+ )
406
+ )
407
+ set_element_text(label_el, option["label"])
408
+ label_id = option.get("label_id")
409
+ if label_id is not None:
410
+ label_el._props["id"] = label_id
411
+ if input_id is not None:
412
+ label_el._props["for"] = input_id
413
+ _apply_style_dict(
414
+ label_el,
415
+ _merged_style(label_style, label_checked_style, use_extra=checked),
416
+ )
417
+ return wrapper, input_el, label_el
418
+
419
+
420
+ class _SelectImpl(BootstrapValueElement):
421
+ """Private Select implementation."""
422
+
423
+ component_name = "Select"
424
+ children_kind = "grouping"
425
+ reserved_classes = ("form-select",)
426
+
427
+ def __init__(
428
+ self,
429
+ children: object = None,
430
+ *,
431
+ _surface: str,
432
+ value: Any = None,
433
+ options: list[Any] | dict[Any, Any] | tuple[Any, ...] | None = None,
434
+ placeholder: str | None = None,
435
+ size: str | None = None,
436
+ html_size: int | None = None,
437
+ disabled: bool = False,
438
+ required: bool = False,
439
+ valid: bool | None = None,
440
+ invalid: bool | None = None,
441
+ name: str | None = None,
442
+ on_change: Callable[..., Any] | None = None,
443
+ **kwargs: Any,
444
+ ) -> None:
445
+ self._surface = _surface
446
+ self._normalized_options = normalize_choice_options(options)
447
+ self._structural_classes = tuple(
448
+ select_structural_classes(size=size, valid=valid, invalid=invalid)
449
+ )
450
+ use_options = options is not None
451
+ super().__init__(
452
+ None if use_options else children,
453
+ value=value,
454
+ on_change=on_change,
455
+ tag="select",
456
+ **kwargs,
457
+ )
458
+ if html_size is not None:
459
+ self._props["size"] = int(html_size)
460
+ _set_flag(self, "disabled", disabled)
461
+ _set_flag(self, "required", required)
462
+ if name is not None:
463
+ self._props["name"] = name
464
+ if use_options:
465
+ current = self.value
466
+ current_html = None if current is None or current == "" else _html_value(current)
467
+ matched = False
468
+ if current_html is not None:
469
+ matched = any(
470
+ _html_value(opt["value"]) == current_html for opt in self._normalized_options
471
+ )
472
+ with self:
473
+ if placeholder is not None:
474
+ _make_option_element(
475
+ value="",
476
+ label=placeholder,
477
+ disabled=True,
478
+ hidden=True,
479
+ selected=not matched,
480
+ )
481
+ for opt in self._normalized_options:
482
+ selected = (
483
+ current_html is not None and _html_value(opt["value"]) == current_html
484
+ )
485
+ _make_option_element(
486
+ value=_html_value(opt["value"]),
487
+ label=opt["label"],
488
+ disabled=bool(opt["disabled"]),
489
+ title=opt["title"],
490
+ selected=selected,
491
+ )
492
+ self.on("change", self._on_dom_change, js_handler=_JS_SELECT_CHANGE)
493
+
494
+ def _on_dom_change(self, e: Any) -> None:
495
+ payload = _payload_from_event(e)
496
+ if isinstance(payload, list):
497
+ payload = payload[0] if payload else ""
498
+ if payload is None or payload == "":
499
+ self.value = None
500
+ return
501
+ self.value = str(payload)
502
+
503
+
504
+ Select, DbcSelect = make_surface_classes("Select", globals())
505
+ select = Select
506
+
507
+
508
+ class _FormCheckControlImpl(BootstrapValueElement):
509
+ """Shared boolean form-check control (checkbox, switch, radio)."""
510
+
511
+ component_name = "Checkbox"
512
+ children_kind = "grouping"
513
+ reserved_classes: tuple[str, ...] = ("form-check",)
514
+ _input_type: str = "checkbox"
515
+ _switch: bool = False
516
+
517
+ def __init__(
518
+ self,
519
+ children: object = None,
520
+ *,
521
+ _surface: str,
522
+ value: bool = False,
523
+ disabled: bool = False,
524
+ label: str | None = None,
525
+ name: str | None = None,
526
+ input_id: str | None = None,
527
+ label_id: str | None = None,
528
+ input_class_name: str | None = None,
529
+ inputClassName: str | None = None,
530
+ input_style: dict[str, Any] | str | None = None,
531
+ label_class_name: str | None = None,
532
+ labelClassName: str | None = None,
533
+ label_style: dict[str, Any] | str | None = None,
534
+ on_change: Callable[..., Any] | None = None,
535
+ **kwargs: Any,
536
+ ) -> None:
537
+ self._surface = _surface
538
+ if _has_children(children):
539
+ raise ChildrenError(
540
+ f"{self.component_name} does not accept children; use the label property instead"
541
+ )
542
+ self._structural_classes = tuple(form_check_item_classes(switch=self._switch))
543
+ super().__init__(
544
+ None,
545
+ value=bool(value),
546
+ on_change=on_change,
547
+ tag="div",
548
+ **kwargs,
549
+ )
550
+ resolved_input_id = input_id or f"nbc-{self.component_name.lower()}-{self.id}"
551
+ self._input, self._label = _mount_form_check(
552
+ self,
553
+ input_type=self._input_type,
554
+ checked=bool(self.value),
555
+ disabled=disabled,
556
+ label=label,
557
+ name=name,
558
+ input_id=resolved_input_id,
559
+ label_id=label_id,
560
+ input_class_name=_resolve_alias(input_class_name, inputClassName),
561
+ input_style=input_style,
562
+ label_class_name=_resolve_alias(label_class_name, labelClassName),
563
+ label_style=label_style,
564
+ )
565
+ self.on("change", self._on_dom_change, js_handler=_JS_CHECK_CHANGE)
566
+
567
+ def _handle_value_change(self, *args: Any, **kwargs: Any) -> None:
568
+ inherited = getattr(super(), "_handle_value_change", None)
569
+ if callable(inherited):
570
+ inherited(*args, **kwargs)
571
+ self._sync_to_dom()
572
+
573
+ def _sync_to_dom(self) -> None:
574
+ input_el = getattr(self, "_input", None)
575
+ if input_el is None:
576
+ return
577
+ _set_flag(input_el, "checked", bool(self.value))
578
+ updater = getattr(input_el, "update", None)
579
+ if callable(updater):
580
+ updater()
581
+
582
+ def _on_dom_change(self, e: Any) -> None:
583
+ payload = _payload_from_event(e)
584
+ if isinstance(payload, list):
585
+ payload = payload[0] if payload else False
586
+ self.value = bool(payload)
587
+
588
+
589
+ class _CheckboxImpl(_FormCheckControlImpl):
590
+ """Private Checkbox implementation."""
591
+
592
+ component_name = "Checkbox"
593
+
594
+
595
+ class _SwitchImpl(_FormCheckControlImpl):
596
+ """Private Switch implementation."""
597
+
598
+ component_name = "Switch"
599
+ reserved_classes = ("form-check", "form-switch")
600
+ _switch = True
601
+
602
+
603
+ class _RadioButtonImpl(_FormCheckControlImpl):
604
+ """Private RadioButton implementation."""
605
+
606
+ component_name = "RadioButton"
607
+ _input_type = "radio"
608
+
609
+
610
+ Checkbox, DbcCheckbox = make_surface_classes("Checkbox", globals())
611
+ checkbox = Checkbox
612
+
613
+ Switch, DbcSwitch = make_surface_classes("Switch", globals())
614
+ switch = Switch
615
+
616
+ RadioButton, DbcRadioButton = make_surface_classes("RadioButton", globals())
617
+ radio_button = RadioButton
618
+
619
+
620
+ class _ChoiceGroupImpl(BootstrapValueElement):
621
+ """Shared options group for Checklist and RadioItems."""
622
+
623
+ component_name = "Checklist"
624
+ children_kind = "grouping"
625
+ reserved_classes: tuple[str, ...] = ()
626
+ _input_type: str = "checkbox"
627
+ _multi: bool = True
628
+
629
+ def __init__(
630
+ self,
631
+ children: object = None,
632
+ *,
633
+ _surface: str,
634
+ value: Any = None,
635
+ options: list[Any] | dict[Any, Any] | tuple[Any, ...] | None = None,
636
+ inline: bool = False,
637
+ switch: bool = False,
638
+ disabled: bool = False,
639
+ name: str | None = None,
640
+ input_class_name: str | None = None,
641
+ inputClassName: str | None = None,
642
+ input_style: dict[str, Any] | str | None = None,
643
+ input_checked_class_name: str | None = None,
644
+ inputCheckedClassName: str | None = None,
645
+ input_checked_style: dict[str, Any] | str | None = None,
646
+ inputCheckedStyle: dict[str, Any] | str | None = None,
647
+ label_class_name: str | None = None,
648
+ labelClassName: str | None = None,
649
+ label_style: dict[str, Any] | str | None = None,
650
+ label_checked_class_name: str | None = None,
651
+ labelCheckedClassName: str | None = None,
652
+ label_checked_style: dict[str, Any] | str | None = None,
653
+ labelCheckedStyle: dict[str, Any] | str | None = None,
654
+ on_change: Callable[..., Any] | None = None,
655
+ **kwargs: Any,
656
+ ) -> None:
657
+ self._surface = _surface
658
+ if _has_children(children):
659
+ raise ChildrenError(
660
+ f"{self.component_name} does not accept children; use the options property instead"
661
+ )
662
+ self._normalized_options = normalize_choice_options(options)
663
+ if self._multi:
664
+ incoming = list(value) if value is not None else []
665
+ value = _filter_selected(incoming, self._normalized_options)
666
+ elif value is not None:
667
+ value = _match_option_value(value, self._normalized_options)
668
+ self._structural_classes = ()
669
+ self._inline = inline
670
+ self._switch = switch
671
+ self._name = name
672
+ self._input_class_name = _resolve_alias(input_class_name, inputClassName)
673
+ self._input_style = input_style
674
+ self._input_checked_class_name = _resolve_alias(
675
+ input_checked_class_name, inputCheckedClassName
676
+ )
677
+ self._input_checked_style = _resolve_alias(input_checked_style, inputCheckedStyle)
678
+ self._label_class_name = _resolve_alias(label_class_name, labelClassName)
679
+ self._label_style = label_style
680
+ self._label_checked_class_name = _resolve_alias(
681
+ label_checked_class_name, labelCheckedClassName
682
+ )
683
+ self._label_checked_style = _resolve_alias(label_checked_style, labelCheckedStyle)
684
+ self._html_to_value = {
685
+ _html_value(opt["value"]): opt["value"] for opt in self._normalized_options
686
+ }
687
+ super().__init__(None, value=value, on_change=on_change, tag="div", **kwargs)
688
+ group_name = self._name
689
+ if group_name is None and not self._multi:
690
+ group_name = f"nbc-radioitems-{self.id}"
691
+ self._item_refs: list[tuple[Element, Element, dict[str, Any]]] = []
692
+ for index, opt in enumerate(self._normalized_options):
693
+ checked = _option_is_selected(opt["value"], self.value, multi=self._multi)
694
+ option_disabled = bool(opt["disabled"]) or disabled
695
+ option = dict(opt)
696
+ option["disabled"] = option_disabled
697
+ input_id = opt["input_id"] or f"nbc-{self.component_name.lower()}-{self.id}-{index}"
698
+ _wrapper, input_el, label_el = _mount_choice_item(
699
+ self,
700
+ input_type=self._input_type,
701
+ option=option,
702
+ checked=checked,
703
+ inline=self._inline,
704
+ switch=self._switch,
705
+ name=group_name,
706
+ input_class_name=self._input_class_name,
707
+ input_checked_class_name=self._input_checked_class_name,
708
+ input_style=self._input_style,
709
+ input_checked_style=self._input_checked_style,
710
+ label_class_name=self._label_class_name,
711
+ label_checked_class_name=self._label_checked_class_name,
712
+ label_style=self._label_style,
713
+ label_checked_style=self._label_checked_style,
714
+ input_id=input_id,
715
+ )
716
+ self._item_refs.append((input_el, label_el, opt))
717
+ js_handler = _JS_CHECKLIST_CHANGE if self._multi else _JS_RADIO_ITEMS_CHANGE
718
+ self.on("change", self._on_dom_change, js_handler=js_handler)
719
+
720
+ def _restore_value(self, raw: Any) -> Any:
721
+ text = _html_value(raw)
722
+ if text in self._html_to_value:
723
+ return self._html_to_value[text]
724
+ return raw
725
+
726
+ def _handle_value_change(self, *args: Any, **kwargs: Any) -> None:
727
+ inherited = getattr(super(), "_handle_value_change", None)
728
+ if callable(inherited):
729
+ inherited(*args, **kwargs)
730
+ self._sync_to_dom()
731
+
732
+ def _sync_to_dom(self) -> None:
733
+ items = getattr(self, "_item_refs", None)
734
+ if not items:
735
+ return
736
+ current = self.value
737
+ for input_el, _label_el, opt in items:
738
+ checked = _option_is_selected(opt["value"], current, multi=self._multi)
739
+ _set_flag(input_el, "checked", checked)
740
+ updater = getattr(input_el, "update", None)
741
+ if callable(updater):
742
+ updater()
743
+
744
+ def _on_dom_change(self, e: Any) -> None:
745
+ payload = _payload_from_event(e)
746
+ if self._multi:
747
+ if payload is None:
748
+ payload = []
749
+ if not isinstance(payload, list):
750
+ payload = [payload]
751
+ self.value = [self._restore_value(item) for item in payload]
752
+ return
753
+ if isinstance(payload, list):
754
+ payload = payload[0] if payload else None
755
+ if payload is None or payload == "":
756
+ self.value = None
757
+ return
758
+ self.value = self._restore_value(payload)
759
+
760
+
761
+ class _ChecklistImpl(_ChoiceGroupImpl):
762
+ """Private Checklist implementation."""
763
+
764
+ component_name = "Checklist"
765
+
766
+
767
+ class _RadioItemsImpl(_ChoiceGroupImpl):
768
+ """Private RadioItems implementation."""
769
+
770
+ component_name = "RadioItems"
771
+ _input_type = "radio"
772
+ _multi = False
773
+
774
+
775
+ Checklist, DbcChecklist = make_surface_classes("Checklist", globals())
776
+ checklist = Checklist
777
+
778
+ RadioItems, DbcRadioItems = make_surface_classes("RadioItems", globals())
779
+ radio_items = RadioItems