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,28 @@
1
+ """Native ``bs.scope`` island wrapper (Mode B Reboot / selector anchor)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .._base import BootstrapElement
8
+
9
+ __all__ = ["Scope", "scope"]
10
+
11
+
12
+ class Scope(BootstrapElement):
13
+ """Mode B island / Reboot scope-anchor. Tag ``div``, class ``ngbs``.
14
+
15
+ Individual components do not carry ``ngbs``; Bootstrap selectors are rewritten
16
+ against this ancestor (and the overlay root).
17
+ """
18
+
19
+ component_name = "Scope"
20
+ children_kind = "auto"
21
+
22
+ def __init__(self, children: object = None, **props: Any) -> None:
23
+ self._surface = "native"
24
+ self._structural_classes = ("ngbs",)
25
+ super().__init__(children, tag="div", **props)
26
+
27
+
28
+ scope = Scope
@@ -0,0 +1,605 @@
1
+ """Tabs family: Tabs and Tab."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+ from functools import partial
7
+ from typing import Any
8
+
9
+ from nicegui import ui
10
+ from nicegui.element import Element
11
+
12
+ from .._base import (
13
+ BootstrapElement,
14
+ ChildrenError,
15
+ UnsupportedPropError,
16
+ make_surface_classes,
17
+ normalize_style,
18
+ resolve_class_name,
19
+ )
20
+
21
+ __all__ = [
22
+ "Tabs",
23
+ "DbcTabs",
24
+ "tabs",
25
+ "Tab",
26
+ "DbcTab",
27
+ "tab",
28
+ "assign_tab_ids",
29
+ "first_enabled_tab_id",
30
+ "tab_button_aria",
31
+ "tab_link_classes",
32
+ "tab_pane_aria",
33
+ "tab_pane_classes",
34
+ "tabs_card_body_classes",
35
+ "tabs_card_header_classes",
36
+ "tabs_content_classes",
37
+ "tabs_nav_classes",
38
+ "tabs_nav_item_classes",
39
+ "tabs_structural_classes",
40
+ ]
41
+
42
+ _PERSIST_VALUES = ("off", "local", "session", "memory")
43
+ _ACTIVE_TAB_MEMORY: dict[str, str] = {}
44
+
45
+ _TABLIST_KEYDOWN = """(e) => {
46
+ const key = e.key;
47
+ if (key !== 'ArrowLeft' && key !== 'ArrowRight' && key !== 'Home' && key !== 'End') return;
48
+ const root = e.currentTarget;
49
+ if (!root || !root.querySelectorAll) return;
50
+ const tabs = Array.from(root.querySelectorAll('[role="tab"]')).filter((el) => {
51
+ return !el.disabled && el.getAttribute('aria-disabled') !== 'true'
52
+ && !el.classList.contains('disabled');
53
+ });
54
+ if (!tabs.length) return;
55
+ let idx = tabs.indexOf(document.activeElement);
56
+ if (key === 'Home') idx = 0;
57
+ else if (key === 'End') idx = tabs.length - 1;
58
+ else if (key === 'ArrowLeft') idx = idx <= 0 ? tabs.length - 1 : idx - 1;
59
+ else if (key === 'ArrowRight') idx = (idx + 1) % tabs.length;
60
+ e.preventDefault();
61
+ const next = tabs[idx];
62
+ if (next) { next.focus(); next.click(); }
63
+ }"""
64
+
65
+
66
+ def tabs_structural_classes(*, card: bool = False) -> list[str]:
67
+ """Return structural classes for the Tabs root."""
68
+ if card:
69
+ return ["card"]
70
+ return []
71
+
72
+
73
+ def tabs_nav_classes(*, card: bool = False) -> list[str]:
74
+ """Return Bootstrap ``nav nav-tabs`` classes, with card-header-tabs when card."""
75
+ classes = ["nav", "nav-tabs"]
76
+ if card:
77
+ classes.append("card-header-tabs")
78
+ return classes
79
+
80
+
81
+ def tabs_nav_item_classes() -> list[str]:
82
+ """Return Bootstrap classes for a tab list item."""
83
+ return ["nav-item"]
84
+
85
+
86
+ def tabs_card_header_classes() -> list[str]:
87
+ """Return Bootstrap card header classes for card tabs."""
88
+ return ["card-header"]
89
+
90
+
91
+ def tabs_card_body_classes() -> list[str]:
92
+ """Return Bootstrap card body classes for card tabs."""
93
+ return ["card-body"]
94
+
95
+
96
+ def tabs_content_classes() -> list[str]:
97
+ """Return Bootstrap tab content wrapper classes."""
98
+ return ["tab-content"]
99
+
100
+
101
+ def tab_link_classes(*, active: bool = False, disabled: bool = False) -> list[str]:
102
+ """Return Bootstrap ``nav-link`` classes for a tab button."""
103
+ classes = ["nav-link"]
104
+ if active:
105
+ classes.append("active")
106
+ if disabled:
107
+ classes.append("disabled")
108
+ return classes
109
+
110
+
111
+ def tab_pane_classes(*, active: bool = False) -> list[str]:
112
+ """Return Bootstrap ``tab-pane`` classes."""
113
+ classes = ["tab-pane"]
114
+ if active:
115
+ classes.append("active")
116
+ return classes
117
+
118
+
119
+ def assign_tab_ids(tab_ids: Sequence[str | None]) -> list[str]:
120
+ """Return stable tab ids for the given child order.
121
+
122
+ Missing ids become ``tab-0``, ``tab-1``, ... using the child index when free,
123
+ then the lowest unused ``tab-n`` if that index collides with an explicit id.
124
+ Existing ids are preserved.
125
+ """
126
+ used = {item for item in tab_ids if item}
127
+ result: list[str] = []
128
+ for index, current in enumerate(tab_ids):
129
+ if current:
130
+ result.append(current)
131
+ continue
132
+ candidate = f"tab-{index}"
133
+ if candidate in used:
134
+ n = 0
135
+ while f"tab-{n}" in used:
136
+ n += 1
137
+ candidate = f"tab-{n}"
138
+ used.add(candidate)
139
+ result.append(candidate)
140
+ return result
141
+
142
+
143
+ def first_enabled_tab_id(tab_ids: Sequence[str], disabled: Sequence[bool]) -> str | None:
144
+ """Return the first tab id whose matching disabled flag is false."""
145
+ for tab_id, is_disabled in zip(tab_ids, disabled, strict=True):
146
+ if not is_disabled:
147
+ return tab_id
148
+ return None
149
+
150
+
151
+ def tab_button_aria(
152
+ *,
153
+ tab_id: str,
154
+ pane_id: str,
155
+ selected: bool,
156
+ disabled: bool = False,
157
+ ) -> dict[str, str]:
158
+ """Return ARIA attributes for a tab button."""
159
+ attrs = {
160
+ "role": "tab",
161
+ "id": f"{tab_id}-tab",
162
+ "aria-controls": pane_id,
163
+ "aria-selected": "true" if selected else "false",
164
+ "tabindex": "0" if selected and not disabled else "-1",
165
+ }
166
+ if disabled:
167
+ attrs["aria-disabled"] = "true"
168
+ return attrs
169
+
170
+
171
+ def tab_pane_aria(*, tab_id: str, pane_id: str, selected: bool) -> dict[str, str]:
172
+ """Return ARIA attributes for a tab pane."""
173
+ return {
174
+ "role": "tabpanel",
175
+ "id": pane_id,
176
+ "aria-labelledby": f"{tab_id}-tab",
177
+ "aria-hidden": "false" if selected else "true",
178
+ }
179
+
180
+
181
+ def _is_tab_element(value: object) -> bool:
182
+ return getattr(type(value), "component_name", None) == "Tab"
183
+
184
+
185
+ def _element_children(element: Any) -> list[Any]:
186
+ slot = getattr(element, "default_slot", None)
187
+ if slot is None:
188
+ return []
189
+ children = getattr(slot, "children", None)
190
+ if not children:
191
+ return []
192
+ return list(children)
193
+
194
+
195
+ def _coerce_tab_children(children: object) -> list[Any]:
196
+ if children is None:
197
+ return []
198
+ if _is_tab_element(children):
199
+ return [children]
200
+ if isinstance(children, (str, bytes, bytearray)):
201
+ raise ChildrenError("Tabs does not accept text children; wrap text in a Tab component.")
202
+ if isinstance(children, Sequence):
203
+ items = list(children)
204
+ else:
205
+ raise ChildrenError(
206
+ "Tabs only accepts Tab children; wrap other content in a Tab component."
207
+ )
208
+ result: list[Any] = []
209
+ for child in items:
210
+ if isinstance(child, str):
211
+ raise ChildrenError("Tabs does not accept text children; wrap text in a Tab component.")
212
+ if not _is_tab_element(child):
213
+ raise ChildrenError(
214
+ "Tabs only accepts Tab children; wrap other content in a Tab component."
215
+ )
216
+ result.append(child)
217
+ return result
218
+
219
+
220
+ def _set_class(element: Any, name: str, present: bool) -> None:
221
+ if present:
222
+ element.classes(add=name)
223
+ else:
224
+ element.classes(remove=name)
225
+
226
+
227
+ def _apply_style(element: Any, style: dict[str, Any] | str | None) -> None:
228
+ if style is None:
229
+ return
230
+ if isinstance(style, dict):
231
+ element.style(normalize_style(style))
232
+ return
233
+ element.style(str(style))
234
+
235
+
236
+ def _pane_dom_id(tab: Any) -> str:
237
+ current = tab._props.get("id")
238
+ if isinstance(current, str) and current:
239
+ return current
240
+ assigned = tab.tab_id or "tab"
241
+ tab._props["id"] = assigned
242
+ return assigned
243
+
244
+
245
+ class _TabImpl(BootstrapElement):
246
+ """Private Tab implementation. Renders as a pane; the parent Tabs owns the tab button."""
247
+
248
+ component_name = "Tab"
249
+ children_kind = "auto"
250
+ reserved_classes = ("tab-pane",)
251
+
252
+ def __init__(
253
+ self,
254
+ children: object = None,
255
+ *,
256
+ _surface: str,
257
+ label: str | None = None,
258
+ tab_id: str | None = None,
259
+ disabled: bool = False,
260
+ label_class_name: str | None = None,
261
+ label_style: dict[str, Any] | str | None = None,
262
+ **kwargs: Any,
263
+ ) -> None:
264
+ self._surface = _surface
265
+ label_class_alias = kwargs.pop("labelClassName", None)
266
+ self._label_class_name = resolve_class_name(
267
+ label_class_name, label_class_alias, self._surface
268
+ )
269
+ tab_class_name = kwargs.pop("tab_class_name", None)
270
+ tab_class_alias = kwargs.pop("tabClassName", None)
271
+ self._tab_class_name = resolve_class_name(tab_class_name, tab_class_alias, self._surface)
272
+ self._tab_style = kwargs.pop("tab_style", None)
273
+ kwargs.pop("tabStyle", None)
274
+ for extra in (
275
+ "key",
276
+ "loading_state",
277
+ "active_label_style",
278
+ "active_label_class_name",
279
+ "active_labelClassName",
280
+ "active_tab_class_name",
281
+ "active_tab_style",
282
+ "active_tabClassName",
283
+ "active_tab_className",
284
+ ):
285
+ kwargs.pop(extra, None)
286
+ self._label = label
287
+ self._explicit_tab_id = tab_id if tab_id else None
288
+ self._tab_id = self._explicit_tab_id
289
+ self._disabled = bool(disabled)
290
+ self._label_style = label_style
291
+ self._structural_classes = tuple(tab_pane_classes(active=False))
292
+ super().__init__(children, tag="div", **kwargs)
293
+ self._props["role"] = "tabpanel"
294
+
295
+ @property
296
+ def tab_id(self) -> str | None:
297
+ return self._tab_id
298
+
299
+ @property
300
+ def label(self) -> str | None:
301
+ return self._label
302
+
303
+ @property
304
+ def disabled(self) -> bool:
305
+ return self._disabled
306
+
307
+
308
+ class _TabsImpl(BootstrapElement):
309
+ """Private Tabs implementation. Tab children become a tablist plus panes."""
310
+
311
+ component_name = "Tabs"
312
+ children_kind = "grouping"
313
+ reserved_classes: tuple[str, ...] = ()
314
+
315
+ def __init__(
316
+ self,
317
+ children: object = None,
318
+ *,
319
+ _surface: str,
320
+ active_tab: str | None = None,
321
+ card: bool = False,
322
+ **kwargs: Any,
323
+ ) -> None:
324
+ self._surface = _surface
325
+ persist, lazy, on_change = self._consume_surface_kwargs(kwargs)
326
+ parsed = _coerce_tab_children(children)
327
+ self._card = bool(card)
328
+ self._lazy = lazy
329
+ self._persist = persist
330
+ self._on_change = on_change
331
+ self._requested_active = active_tab
332
+ self._active_tab = active_tab
333
+ self._tabs: list[Any] = []
334
+ self._buttons: dict[str, Element] = {}
335
+ self._nav_items: dict[str, Element] = {}
336
+ self._lazy_held: list[Any] = []
337
+ self._header: Element | None = None
338
+ self._body: Element | None = None
339
+ self._structural_classes = tuple(tabs_structural_classes(card=self._card))
340
+ super().__init__(None, tag="div", **kwargs)
341
+ self._build_chrome()
342
+ self._tabs.extend(parsed)
343
+ assigned = assign_tab_ids([tab._explicit_tab_id for tab in self._tabs])
344
+ for tab, tab_id in zip(self._tabs, assigned, strict=True):
345
+ tab._tab_id = tab_id
346
+ self._restore_persist()
347
+ self._ensure_active()
348
+ for tab in self._tabs:
349
+ self._mount_pane(tab)
350
+ self._create_nav_item(tab)
351
+ self._apply_active_classes()
352
+ self._persist_save()
353
+
354
+ def _consume_surface_kwargs(
355
+ self,
356
+ kwargs: dict[str, Any],
357
+ ) -> tuple[str, bool, Callable[..., Any] | None]:
358
+ if self._surface == "compat":
359
+ for native_only in ("lazy", "on_change", "persist"):
360
+ if native_only in kwargs:
361
+ raise UnsupportedPropError(native_only)
362
+ persistence = kwargs.pop("persistence", None)
363
+ kwargs.pop("persisted_props", None)
364
+ persistence_type = kwargs.pop("persistence_type", None)
365
+ kwargs.pop("key", None)
366
+ kwargs.pop("loading_state", None)
367
+ persist = "off"
368
+ if persistence:
369
+ if persistence_type in {"local", "session", "memory"}:
370
+ persist = str(persistence_type)
371
+ else:
372
+ persist = "local"
373
+ return persist, False, None
374
+ persist = kwargs.pop("persist", "off")
375
+ if persist not in _PERSIST_VALUES:
376
+ allowed = ", ".join(repr(item) for item in _PERSIST_VALUES)
377
+ raise ValueError(f"persist must be one of {allowed}, got {persist!r}")
378
+ lazy = bool(kwargs.pop("lazy", False))
379
+ on_change = kwargs.pop("on_change", None)
380
+ return str(persist), lazy, on_change
381
+
382
+ def _build_chrome(self) -> None:
383
+ with self:
384
+ if self._card:
385
+ self._header = ui.element("div")
386
+ self._header.classes(" ".join(tabs_card_header_classes()))
387
+ with self._header:
388
+ self._nav = ui.element("ul")
389
+ self._nav.classes(" ".join(tabs_nav_classes(card=True)))
390
+ self._body = ui.element("div")
391
+ self._body.classes(" ".join(tabs_card_body_classes()))
392
+ with self._body:
393
+ self._content = ui.element("div")
394
+ self._content.classes(" ".join(tabs_content_classes()))
395
+ else:
396
+ self._nav = ui.element("ul")
397
+ self._nav.classes(" ".join(tabs_nav_classes(card=False)))
398
+ self._content = ui.element("div")
399
+ self._content.classes(" ".join(tabs_content_classes()))
400
+ self._nav._props["role"] = "tablist"
401
+ self._nav.on("keydown", self._on_tablist_keydown, js_handler=_TABLIST_KEYDOWN)
402
+
403
+ def _on_tablist_keydown(self, *_args: Any, **_kwargs: Any) -> None:
404
+ return None
405
+
406
+ def __exit__(self, *args: Any) -> Any:
407
+ result = super().__exit__(*args)
408
+ self._sync_tabs()
409
+ return result
410
+
411
+ @property
412
+ def active_tab(self) -> str | None:
413
+ self._sync_tabs()
414
+ return self._active_tab
415
+
416
+ @active_tab.setter
417
+ def active_tab(self, value: str | None) -> None:
418
+ self._sync_tabs()
419
+ self._requested_active = value
420
+ self._active_tab = value
421
+ if value is None:
422
+ self._ensure_active()
423
+ self._apply_active_classes()
424
+ self._persist_save()
425
+
426
+ @property
427
+ def tab_ids(self) -> list[str]:
428
+ self._sync_tabs()
429
+ return [tab.tab_id for tab in self._tabs if tab.tab_id is not None]
430
+
431
+ def _persist_key(self) -> str | None:
432
+ public = self._props.get("id")
433
+ if isinstance(public, str) and public:
434
+ return public
435
+ return None
436
+
437
+ def _restore_persist(self) -> None:
438
+ if self._persist == "off" or self._requested_active is not None:
439
+ return
440
+ key = self._persist_key()
441
+ if key is None:
442
+ return
443
+ remembered = _ACTIVE_TAB_MEMORY.get(key)
444
+ known = {tab.tab_id for tab in self._tabs}
445
+ if remembered and remembered in known:
446
+ self._active_tab = remembered
447
+
448
+ def _persist_save(self) -> None:
449
+ if self._persist == "off" or self._active_tab is None:
450
+ return
451
+ key = self._persist_key()
452
+ if key is not None:
453
+ _ACTIVE_TAB_MEMORY[key] = self._active_tab
454
+
455
+ def _ensure_active(self) -> None:
456
+ ids = [tab.tab_id for tab in self._tabs if tab.tab_id is not None]
457
+ disabled = [tab.disabled for tab in self._tabs]
458
+ if self._active_tab is not None and self._active_tab in ids:
459
+ return
460
+ self._active_tab = first_enabled_tab_id(ids, disabled)
461
+
462
+ def _find_tab(self, tab_id: str) -> Any | None:
463
+ for tab in self._tabs:
464
+ if tab.tab_id == tab_id:
465
+ return tab
466
+ return None
467
+
468
+ def _mount_pane(self, tab: Any) -> None:
469
+ selected = tab.tab_id == self._active_tab
470
+ if self._lazy and not selected:
471
+ if tab not in self._lazy_held:
472
+ self._lazy_held.append(tab)
473
+ return
474
+ if tab in self._lazy_held:
475
+ self._lazy_held.remove(tab)
476
+ tab.move(self._content)
477
+ _set_class(tab, "active", selected)
478
+
479
+ def _create_nav_item(self, tab: Any) -> None:
480
+ tab_id = tab.tab_id
481
+ if tab_id is None or tab_id in self._buttons:
482
+ return
483
+ selected = tab_id == self._active_tab
484
+ with self._nav:
485
+ item = ui.element("li")
486
+ item.classes(" ".join(tabs_nav_item_classes()))
487
+ item._props["role"] = "presentation"
488
+ with item:
489
+ btn = ui.element("button")
490
+ btn.classes(" ".join(tab_link_classes(active=selected, disabled=tab.disabled)))
491
+ btn._props["type"] = "button"
492
+ if tab._tab_class_name:
493
+ btn.classes(tab._tab_class_name)
494
+ if tab._label_class_name:
495
+ btn.classes(tab._label_class_name)
496
+ _apply_style(btn, tab._tab_style)
497
+ _apply_style(btn, tab._label_style)
498
+ if tab.disabled:
499
+ btn._props["disabled"] = True
500
+ with btn:
501
+ ui.label(tab.label or "")
502
+ btn.on("click", partial(self._on_tab_click, tab_id))
503
+ self._nav_items[tab_id] = item
504
+ self._buttons[tab_id] = btn
505
+
506
+ def _on_tab_click(self, tab_id: str, *_args: Any, **_kwargs: Any) -> None:
507
+ target = self._find_tab(tab_id)
508
+ if target is None or target.disabled:
509
+ return
510
+ self._activate(tab_id, from_user=True)
511
+
512
+ def _activate(self, tab_id: str, *, from_user: bool) -> None:
513
+ if tab_id == self._active_tab:
514
+ return
515
+ self._active_tab = tab_id
516
+ if self._lazy:
517
+ tab = self._find_tab(tab_id)
518
+ if tab is not None and tab in self._lazy_held:
519
+ self._mount_pane(tab)
520
+ self._apply_active_classes()
521
+ self._persist_save()
522
+ if from_user and self._on_change is not None:
523
+ self._on_change(tab_id)
524
+
525
+ def _apply_active_classes(self) -> None:
526
+ for tab in self._tabs:
527
+ tab_id = tab.tab_id
528
+ if tab_id is None:
529
+ continue
530
+ selected = tab_id == self._active_tab
531
+ pane_id = _pane_dom_id(tab)
532
+ for key, value in tab_pane_aria(
533
+ tab_id=tab_id,
534
+ pane_id=pane_id,
535
+ selected=selected,
536
+ ).items():
537
+ tab._props[key] = value
538
+ _set_class(tab, "active", selected)
539
+ tab.update()
540
+ btn = self._buttons.get(tab_id)
541
+ if btn is None:
542
+ continue
543
+ for key, value in tab_button_aria(
544
+ tab_id=tab_id,
545
+ pane_id=pane_id,
546
+ selected=selected,
547
+ disabled=tab.disabled,
548
+ ).items():
549
+ btn._props[key] = value
550
+ _set_class(btn, "active", selected)
551
+ _set_class(btn, "disabled", tab.disabled)
552
+ if tab.disabled:
553
+ btn._props["disabled"] = True
554
+ elif "disabled" in btn._props:
555
+ del btn._props["disabled"]
556
+ btn.update()
557
+
558
+ def _register_new_tab(self, tab: Any) -> None:
559
+ if any(existing is tab for existing in self._tabs):
560
+ return
561
+ incoming = tab.tab_id if tab.tab_id else tab._explicit_tab_id
562
+ assigned = assign_tab_ids([existing.tab_id for existing in self._tabs] + [incoming])
563
+ tab._tab_id = assigned[-1]
564
+ self._tabs.append(tab)
565
+ if self._active_tab is None:
566
+ self._ensure_active()
567
+ self._mount_pane(tab)
568
+ self._create_nav_item(tab)
569
+ self._apply_active_classes()
570
+
571
+ def _forget_tab(self, tab: Any) -> None:
572
+ tab_id = tab.tab_id
573
+ if tab_id is not None:
574
+ item = self._nav_items.pop(tab_id, None)
575
+ self._buttons.pop(tab_id, None)
576
+ if item is not None:
577
+ item.delete()
578
+ if tab in self._lazy_held:
579
+ self._lazy_held.remove(tab)
580
+ if tab in self._tabs:
581
+ self._tabs.remove(tab)
582
+
583
+ def _sync_tabs(self) -> None:
584
+ if getattr(self, "_content", None) is None:
585
+ return
586
+ loose = [child for child in _element_children(self) if _is_tab_element(child)]
587
+ for tab in loose:
588
+ self._register_new_tab(tab)
589
+ present = [child for child in _element_children(self._content) if _is_tab_element(child)]
590
+ present.extend(self._lazy_held)
591
+ present_ids = {id(tab) for tab in present}
592
+ for tab in list(self._tabs):
593
+ if id(tab) not in present_ids:
594
+ self._forget_tab(tab)
595
+ known_ids = {tab.tab_id for tab in self._tabs}
596
+ if self._active_tab not in known_ids:
597
+ self._ensure_active()
598
+ self._apply_active_classes()
599
+ self._persist_save()
600
+
601
+
602
+ Tab, DbcTab = make_surface_classes("Tab", globals())
603
+ tab = Tab
604
+ Tabs, DbcTabs = make_surface_classes("Tabs", globals())
605
+ tabs = Tabs