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,7 @@
1
+ from nicegui_bootstrap_components import bs, dbc, icons, themes
2
+ from nicegui_bootstrap_components.assets import StyleMode, setup
3
+ from nicegui_bootstrap_components.theme import ThemeController
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["StyleMode", "ThemeController", "bs", "dbc", "icons", "setup", "themes", "__version__"]
@@ -0,0 +1,590 @@
1
+ """Shared Bootstrap element contract, dual-surface factory, and normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import warnings
7
+ from collections.abc import Callable, Mapping, MutableMapping
8
+ from dataclasses import dataclass
9
+ from typing import Any, Literal, cast
10
+
11
+ from ._host import (
12
+ Element,
13
+ ValueElement,
14
+ client_store,
15
+ create_text_span,
16
+ get_parent,
17
+ get_slot_parent,
18
+ is_deleted,
19
+ is_ui_element,
20
+ move_element,
21
+ set_element_text,
22
+ )
23
+
24
+ __all__ = [
25
+ "BootstrapElement",
26
+ "BootstrapElementMixin",
27
+ "BootstrapValueElement",
28
+ "ChildrenError",
29
+ "OverlayElement",
30
+ "PropConflictError",
31
+ "SharedProps",
32
+ "UnsupportedPropError",
33
+ "_normalize_children",
34
+ "lookup_public_id",
35
+ "make_surface_classes",
36
+ "normalize_style",
37
+ "prepare_shared_props",
38
+ "reject_or_warn",
39
+ "resolve_class_name",
40
+ "style_to_css",
41
+ ]
42
+
43
+ SurfaceName = Literal["native", "compat"]
44
+
45
+ _UNITLESS = frozenset(
46
+ {
47
+ "animation-iteration-count",
48
+ "flex",
49
+ "flex-grow",
50
+ "flex-shrink",
51
+ "font-weight",
52
+ "line-height",
53
+ "opacity",
54
+ "order",
55
+ "z-index",
56
+ "zoom",
57
+ }
58
+ )
59
+
60
+ _KEY_WARNED = False
61
+
62
+
63
+ class PropConflictError(ValueError):
64
+ """Conflicting aliases that would map to different nodes."""
65
+
66
+
67
+ class ChildrenError(ValueError):
68
+ """Illegal children, adoption, or mixed-content combination."""
69
+
70
+
71
+ class UnsupportedPropError(TypeError):
72
+ """Raised on the compat surface for Dash-only or wrong-surface props."""
73
+
74
+ def __init__(self, prop: str, *, component: str | None = None) -> None:
75
+ self.prop = prop
76
+ self.component = component
77
+ if component:
78
+ msg = f"{component}: unsupported prop {prop!r}"
79
+ else:
80
+ msg = f"Unsupported prop {prop!r}"
81
+ super().__init__(msg)
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class SharedProps:
86
+ """Normalized shared constructor fields."""
87
+
88
+ public_id: str | None
89
+ class_name: str | None
90
+ style: dict[str, str] | None
91
+
92
+
93
+ def _reset_process_warnings() -> None:
94
+ global _KEY_WARNED
95
+ _KEY_WARNED = False
96
+
97
+
98
+ def _kebab(name: str) -> str:
99
+ if "-" in name:
100
+ return name.lower()
101
+ chars: list[str] = []
102
+ for i, ch in enumerate(name):
103
+ if ch.isupper() and i > 0:
104
+ chars.append("-")
105
+ chars.append(ch.lower())
106
+ return "".join(chars)
107
+
108
+
109
+ def normalize_style(style: Mapping[str, Any]) -> dict[str, str]:
110
+ """Px-ify numeric CSS values except the unitless allow-list. Keys are preserved."""
111
+ out: dict[str, str] = {}
112
+ for key, value in style.items():
113
+ if value is None:
114
+ continue
115
+ kebab = _kebab(str(key))
116
+ if isinstance(value, bool):
117
+ out[str(key)] = "true" if value else "false"
118
+ continue
119
+ if isinstance(value, (int, float)) and kebab not in _UNITLESS:
120
+ if isinstance(value, float) and value.is_integer():
121
+ out[str(key)] = f"{int(value)}px"
122
+ else:
123
+ out[str(key)] = f"{value}px"
124
+ continue
125
+ out[str(key)] = str(value)
126
+ return out
127
+
128
+
129
+ def style_to_css(style: Mapping[str, str]) -> str:
130
+ """Serialize a normalized style dict to an inline CSS string."""
131
+ return "; ".join(f"{key}: {value}" for key, value in style.items())
132
+
133
+
134
+ def resolve_class_name(
135
+ class_name: str | None,
136
+ className: str | None,
137
+ surface: str,
138
+ ) -> str | None:
139
+ """Resolve ``class_name`` / ``className``. ``class_name`` wins when both nonempty."""
140
+ preferred = class_name if class_name else None
141
+ alias = className if className else None
142
+ if preferred and alias:
143
+ if preferred != alias and surface == "native":
144
+ warnings.warn(
145
+ "Both class_name and className were provided; class_name wins. "
146
+ "className is deprecated.",
147
+ DeprecationWarning,
148
+ stacklevel=2,
149
+ )
150
+ return preferred
151
+ return preferred or alias
152
+
153
+
154
+ def reject_or_warn(surface: str, prop: str, *, component: str | None = None) -> None:
155
+ """Raise on compat; warn and ignore on native."""
156
+ if surface == "compat":
157
+ raise UnsupportedPropError(prop, component=component)
158
+ warnings.warn(
159
+ f"{prop} is not supported on the native surface and will be ignored",
160
+ UserWarning,
161
+ stacklevel=3,
162
+ )
163
+
164
+
165
+ def consume_key(surface: str, props: dict[str, Any]) -> None:
166
+ """Drop Dash/React ``key``. Compat raises; native warns once per process."""
167
+ global _KEY_WARNED
168
+ if "key" not in props:
169
+ return
170
+ value = props.pop("key")
171
+ if value is None:
172
+ return
173
+ if surface == "compat":
174
+ raise UnsupportedPropError("key")
175
+ if not _KEY_WARNED:
176
+ _KEY_WARNED = True
177
+ warnings.warn(
178
+ "`key` is unsupported and ignored on the native surface",
179
+ UserWarning,
180
+ stacklevel=3,
181
+ )
182
+
183
+
184
+ def prepare_shared_props(
185
+ surface: str,
186
+ props: dict[str, Any],
187
+ *,
188
+ component: str | None = None,
189
+ ) -> SharedProps:
190
+ """Pop and normalize shared fields from ``props`` (mutates ``props``)."""
191
+ consume_key(surface, props)
192
+ if "loading_state" in props:
193
+ props.pop("loading_state")
194
+ reject_or_warn(surface, "loading_state", component=component)
195
+ public_id = props.pop("id", None)
196
+ if public_id is not None and not isinstance(public_id, str):
197
+ if isinstance(public_id, (dict, tuple, list)):
198
+ raise TypeError("Pattern-matching ids are not supported")
199
+ public_id = str(public_id)
200
+ class_name = props.pop("class_name", None)
201
+ className = props.pop("className", None)
202
+ style = props.pop("style", None)
203
+ resolved = resolve_class_name(
204
+ class_name if isinstance(class_name, str) or class_name is None else str(class_name),
205
+ className if isinstance(className, str) or className is None else str(className),
206
+ surface,
207
+ )
208
+ if style is None:
209
+ norm_style: dict[str, str] | None = None
210
+ elif isinstance(style, Mapping):
211
+ norm_style = normalize_style(dict(style))
212
+ else:
213
+ raise TypeError(f"style must be a dict, got {type(style)!r}")
214
+ return SharedProps(public_id=public_id, class_name=resolved, style=norm_style)
215
+
216
+
217
+ def _normalize_children(children: object) -> list[object]:
218
+ """Flatten children recursively; drop ``None``; reject unknown types."""
219
+ if children is None:
220
+ return []
221
+ if isinstance(children, bool):
222
+ raise TypeError(f"Unsupported child type: {type(children)!r}")
223
+ if isinstance(children, (str, int, float)) or is_ui_element(children):
224
+ return [children]
225
+ if isinstance(children, (list, tuple)):
226
+ out: list[object] = []
227
+ for child in children:
228
+ out.extend(_normalize_children(child))
229
+ return out
230
+ raise TypeError(f"Unsupported child type: {type(children)!r}")
231
+
232
+
233
+ def _would_cycle(child: Any, new_parent: Any) -> bool:
234
+ if child is new_parent:
235
+ return True
236
+ current: Any | None = new_parent
237
+ seen: set[int] = set()
238
+ while current is not None:
239
+ ident = id(current)
240
+ if ident in seen:
241
+ break
242
+ if current is child:
243
+ return True
244
+ seen.add(ident)
245
+ current = get_parent(current)
246
+ return False
247
+
248
+
249
+ def _to_snake(name: str) -> str:
250
+ out: list[str] = []
251
+ for i, ch in enumerate(name):
252
+ if ch.isupper() and i > 0:
253
+ out.append("_")
254
+ out.append(ch.lower())
255
+ return "".join(out)
256
+
257
+
258
+ def _strip_impl(name: str) -> str:
259
+ trimmed = name[1:] if name.startswith("_") else name
260
+ if trimmed.endswith("Impl"):
261
+ trimmed = trimmed[: -len("Impl")]
262
+ return trimmed
263
+
264
+
265
+ def _make_surface_init(impl_cls: type, surface: str) -> Callable[..., None]:
266
+ impl_init = cast(Any, impl_cls.__init__) # type: ignore[misc]
267
+ params = list(inspect.signature(impl_init).parameters)
268
+ first = params[1] if len(params) > 1 and params[0] == "self" else (params[0] if params else "")
269
+ value_first = first == "value"
270
+
271
+ def __init__(self: Any, children: object = None, *args: Any, **props: Any) -> None:
272
+ init_fn = impl_init
273
+ if value_first:
274
+ init_fn(self, props.pop("value", None), children=children, _surface=surface, **props)
275
+ else:
276
+ init_fn(self, children, _surface=surface, **props)
277
+
278
+ __init__.__doc__ = getattr(impl_init, "__doc__", None)
279
+ return __init__
280
+
281
+
282
+ def make_surface_classes(
283
+ impl_name: str | type,
284
+ native_ns: MutableMapping[str, Any] | None = None,
285
+ compat_ns: MutableMapping[str, Any] | None = None,
286
+ component_name: str | None = None,
287
+ ) -> tuple[type, type]:
288
+ """Create native/compat sibling subclasses of a private impl class.
289
+
290
+ ``self._surface`` is set in each public subclass ``__init__`` (via the
291
+ impl's ``_surface`` argument) before shared validation runs.
292
+
293
+ When ``impl_name`` is a string, the impl is ``native_ns['_{impl_name}Impl']``.
294
+ The native class is stored under PascalCase and snake_case; the compat
295
+ class is stored as ``Dbc{Name}`` on ``native_ns`` and as ``{Name}`` on
296
+ ``compat_ns`` when provided.
297
+ """
298
+ if isinstance(impl_name, str):
299
+ if native_ns is None:
300
+ raise TypeError("native_ns is required when impl_name is a string")
301
+ name = impl_name
302
+ impl_cls = native_ns[f"_{name}Impl"]
303
+ else:
304
+ impl_cls = impl_name
305
+ name = str(
306
+ component_name
307
+ or getattr(impl_cls, "component_name", None)
308
+ or _strip_impl(impl_cls.__name__)
309
+ )
310
+
311
+ native_init = _make_surface_init(impl_cls, "native")
312
+ compat_init = _make_surface_init(impl_cls, "compat")
313
+ native_cls = type(
314
+ name,
315
+ (impl_cls,),
316
+ {
317
+ "__init__": native_init,
318
+ "__module__": impl_cls.__module__,
319
+ "__doc__": impl_cls.__doc__,
320
+ "__qualname__": name,
321
+ "component_name": getattr(impl_cls, "component_name", name),
322
+ },
323
+ )
324
+ compat_cls = type(
325
+ name,
326
+ (impl_cls,),
327
+ {
328
+ "__init__": compat_init,
329
+ "__module__": impl_cls.__module__,
330
+ "__doc__": impl_cls.__doc__,
331
+ "__qualname__": f"Dbc{name}",
332
+ "component_name": getattr(impl_cls, "component_name", name),
333
+ },
334
+ )
335
+ if native_ns is not None:
336
+ native_ns[name] = native_cls
337
+ native_ns[_to_snake(name)] = native_cls
338
+ native_ns[f"Dbc{name}"] = compat_cls
339
+ if compat_ns is not None:
340
+ compat_ns[name] = compat_cls
341
+ return native_cls, compat_cls
342
+
343
+
344
+ def lookup_public_id(public_id: str, *, client: Any | None = None) -> Any | None:
345
+ """Resolve a public (DBC) id via the per-client registry."""
346
+ store = client_store(client)
347
+ registry = store.get("public_ids", {})
348
+ if not isinstance(registry, dict):
349
+ return None
350
+ return registry.get(public_id)
351
+
352
+
353
+ class BootstrapElementMixin:
354
+ """Contract, adoption, public ids, reserved classes. Not an element by itself."""
355
+
356
+ component_name: str = ""
357
+ styling_target: str = "root"
358
+ reserved_classes: tuple[str, ...] = ()
359
+ children_kind: str = "auto"
360
+ _surface: str
361
+
362
+ def __enter__(self) -> Any:
363
+ if getattr(self, "_children_explicit", False):
364
+ raise ChildrenError(
365
+ "Passing children= is not supported when the element is used as a context manager"
366
+ )
367
+ enter = getattr(super(), "__enter__", None)
368
+ if enter is None:
369
+ return self
370
+ return enter()
371
+
372
+ def __exit__(self, *args: Any) -> Any:
373
+ exit_ = getattr(super(), "__exit__", None)
374
+ if exit_ is None:
375
+ return None
376
+ return exit_(*args)
377
+
378
+ def _run_bootstrap_init(
379
+ self,
380
+ children: object,
381
+ props: dict[str, Any],
382
+ *,
383
+ init: Callable[[], None],
384
+ ) -> None:
385
+ surface = props.pop("_surface", None)
386
+ if surface is not None:
387
+ self._surface = surface
388
+ elif not hasattr(self, "_surface"):
389
+ self._surface = "native"
390
+ self._ambient_parent = get_slot_parent()
391
+ self._children_explicit = children is not None
392
+ shared = prepare_shared_props(
393
+ self._surface,
394
+ props,
395
+ component=getattr(self, "component_name", None) or None,
396
+ )
397
+ if props:
398
+ raise TypeError(
399
+ f"{self.component_name or type(self).__name__}: unexpected keyword argument(s): "
400
+ + ", ".join(sorted(str(k) for k in props))
401
+ )
402
+ init()
403
+ self._public_id = shared.public_id
404
+ self._user_class_name = shared.class_name
405
+ self._user_style = shared.style
406
+ self._apply_visuals()
407
+ self._set_public_id(self._public_id)
408
+ if children is not None:
409
+ self._adopt(children)
410
+ from .theme import ensure_theme_bound
411
+
412
+ ensure_theme_bound()
413
+
414
+ def _apply_visuals(self) -> None:
415
+ classes_fn = getattr(self, "classes", None)
416
+ style_fn = getattr(self, "style", None)
417
+ structural = tuple(getattr(self, "_structural_classes", ()))
418
+ if callable(classes_fn):
419
+ for cls in structural:
420
+ if cls:
421
+ classes_fn(cls)
422
+ # ``reserved_classes`` is a protection set (classes owned by the
423
+ # library that must survive user class mutations); it is never
424
+ # applied eagerly — variants like ``is-invalid`` or
425
+ # ``offcanvas-end`` would otherwise all be added at once.
426
+ user = getattr(self, "_user_class_name", None)
427
+ if user:
428
+ classes_fn(user)
429
+ if callable(style_fn):
430
+ user_style = getattr(self, "_user_style", None)
431
+ if user_style:
432
+ style_fn(style_to_css(user_style))
433
+
434
+ def _set_public_id(self, public_id: str | None) -> None:
435
+ self._public_id = public_id
436
+ if not public_id:
437
+ return
438
+ props = getattr(self, "_props", None)
439
+ if props is not None:
440
+ props["data-ngbs-public-id"] = public_id
441
+ try:
442
+ store = client_store(getattr(self, "client", None))
443
+ except Exception:
444
+ return
445
+ registry = store.setdefault("public_ids", {})
446
+ registry[public_id] = self
447
+
448
+ def _validate_adoptee(self, child: Any) -> None:
449
+ if is_deleted(child):
450
+ raise ChildrenError("Cannot adopt a deleted element")
451
+ self_client = getattr(self, "client", None)
452
+ child_client = getattr(child, "client", None)
453
+ if self_client is not None and child_client is not None and child_client is not self_client:
454
+ raise ChildrenError("Cannot adopt an element from a different client")
455
+ if _would_cycle(child, self):
456
+ raise ChildrenError("Adopting this element would create a cycle")
457
+ parent = get_parent(child)
458
+ ambient = getattr(self, "_ambient_parent", None)
459
+ if parent is not None and parent is not ambient and parent is not self:
460
+ raise ChildrenError(
461
+ "Cannot steal an element from an unrelated parent; "
462
+ "only nested-expression adoption is allowed"
463
+ )
464
+
465
+ def _adopt(self, children: object) -> None:
466
+ items = _normalize_children(children)
467
+ if not items:
468
+ return
469
+ coalesced = _coalesce_children(items)
470
+ grouping = getattr(self, "children_kind", "auto") == "grouping"
471
+ if grouping:
472
+ for item in coalesced:
473
+ if isinstance(item, str):
474
+ raise ChildrenError(
475
+ f"{self.component_name}: text children are not allowed. "
476
+ "Wrap text in a component; this parent only allows element children."
477
+ )
478
+ text_only = all(isinstance(item, str) for item in coalesced)
479
+ if text_only and coalesced and not grouping:
480
+ set_element_text(self, "".join(coalesced)) # type: ignore[arg-type]
481
+ return
482
+ seen: set[int] = set()
483
+ for item in coalesced:
484
+ if isinstance(item, str):
485
+ span = create_text_span(item)
486
+ move_element(span, self)
487
+ continue
488
+ ident = id(item)
489
+ if ident in seen:
490
+ raise ChildrenError("Duplicate ownership")
491
+ seen.add(ident)
492
+ self._validate_adoptee(item)
493
+ move_element(item, self)
494
+
495
+
496
+ def _coalesce_children(items: list[object]) -> list[object]:
497
+ out: list[object] = []
498
+ buf: list[str] = []
499
+ for item in items:
500
+ if isinstance(item, bool):
501
+ raise TypeError(f"Unsupported child type: {type(item)!r}")
502
+ if isinstance(item, (str, int, float)):
503
+ piece = str(item)
504
+ if piece:
505
+ buf.append(piece)
506
+ continue
507
+ if buf:
508
+ out.append("".join(buf))
509
+ buf = []
510
+ out.append(item)
511
+ if buf:
512
+ out.append("".join(buf))
513
+ return out
514
+
515
+
516
+ class BootstrapElement(BootstrapElementMixin, Element):
517
+ """Structural or hybrid Bootstrap element."""
518
+
519
+ def __init__(
520
+ self,
521
+ children: object = None,
522
+ *,
523
+ tag: str = "div",
524
+ id: str | None = None,
525
+ class_name: str | None = None,
526
+ className: str | None = None,
527
+ style: Mapping[str, Any] | None = None,
528
+ **compat_props: Any,
529
+ ) -> None:
530
+ if id is not None:
531
+ compat_props.setdefault("id", id)
532
+ if class_name is not None:
533
+ compat_props.setdefault("class_name", class_name)
534
+ if className is not None:
535
+ compat_props.setdefault("className", className)
536
+ if style is not None:
537
+ compat_props.setdefault("style", style)
538
+ self._desired_tag = tag
539
+ self._run_bootstrap_init(
540
+ children,
541
+ compat_props,
542
+ init=lambda: Element.__init__(self, tag),
543
+ )
544
+
545
+
546
+ class BootstrapValueElement(BootstrapElementMixin, ValueElement):
547
+ """Form control with ``.value`` and ``on_change``.
548
+
549
+ Frontend contract: accept prop ``model-value`` and emit ``update:modelValue``.
550
+ Inherits NiceGUI ValueElement (bind_value, loopback protection).
551
+ """
552
+
553
+ def __init__(
554
+ self,
555
+ children: object = None,
556
+ *,
557
+ value: Any = None,
558
+ on_change: Callable[..., Any] | None = None,
559
+ tag: str = "input",
560
+ id: str | None = None,
561
+ class_name: str | None = None,
562
+ className: str | None = None,
563
+ style: Mapping[str, Any] | None = None,
564
+ **props: Any,
565
+ ) -> None:
566
+ if id is not None:
567
+ props.setdefault("id", id)
568
+ if class_name is not None:
569
+ props.setdefault("class_name", class_name)
570
+ if className is not None:
571
+ props.setdefault("className", className)
572
+ if style is not None:
573
+ props.setdefault("style", style)
574
+ self._desired_tag = tag
575
+
576
+ def _init() -> None:
577
+ kwargs: dict[str, Any] = {"value": value}
578
+ if on_change is not None:
579
+ kwargs["on_value_change"] = on_change
580
+ ValueElement.__init__(self, **kwargs)
581
+
582
+ self._run_bootstrap_init(children, props, init=_init)
583
+
584
+
585
+ class OverlayElement(BootstrapElement):
586
+ """Desired vs actual visibility; overlay-root registration is opt-in per subclass."""
587
+
588
+ uses_overlay_root: bool = True
589
+ is_open: bool = False
590
+ phase: str = "closed"